Compare commits

...

52 Commits

Author SHA1 Message Date
Savanni D'Gerinel 81d452694d Reverse the blinker pins 2024-09-15 23:57:06 -04:00
Savanni D'Gerinel 88cf32047b Enable the brake light 2024-09-08 12:53:35 -04:00
Savanni D'Gerinel 6cae7dbb0e Set up a basic server with a device listing endpoint 2024-08-26 10:41:17 -04:00
Savanni D'Gerinel 80776c65d8 Write a program that enumerates audio sinks on the device 2024-08-21 09:40:58 -04:00
Savanni D'Gerinel 1c54e0832b Make a design system page. Build up CSS. 2024-08-20 17:01:36 +00:00
Savanni D'Gerinel aee4528fb3 Rename the Dashboard 2024-08-20 17:01:36 +00:00
Savanni D'Gerinel 0535b6da5a Rename Launcher components 2024-08-20 17:01:36 +00:00
Savanni D'Gerinel b55324feab Add Activator groups 2024-08-20 17:01:36 +00:00
Savanni D'Gerinel 50d8a9670e Start creating some UI components 2024-08-20 17:01:36 +00:00
Savanni D'Gerinel 9cda35e766 UI placeholder 2024-08-20 17:01:36 +00:00
Savanni D'Gerinel d0f461a5eb Create the dashboard placeholder 2024-08-20 17:01:36 +00:00
Savanni D'Gerinel 70c013218a Update pins for the realities of the board layout 2024-07-30 14:50:14 -04:00
Savanni D'Gerinel 37c7e04820 Turn on the built-in LED when software starts up 2024-07-20 11:21:16 -04:00
Savanni D'Gerinel 291663d4a3 Re-add the armv6 toolchain 2024-07-08 09:35:44 -04:00
Savanni D'Gerinel 2b0fc7639e Debounce buttons, fix colors, and add a new water pattern 2024-07-08 09:29:34 -04:00
Savanni D'Gerinel 80d8dedbaf Adjust colors and the blinker patterns 2024-07-08 09:29:34 -04:00
Savanni D'Gerinel d7a70119c8 Send out the full set of lights 2024-07-08 09:29:34 -04:00
Savanni D'Gerinel 54c4b99ab6 Improve the blinker animations and state transitions when switching blinkers 2024-07-08 09:29:34 -04:00
Savanni D'Gerinel ef5415303b Start monitoring events 2024-07-08 09:29:34 -04:00
Savanni D'Gerinel 8d183d6d8c Build some of the framework for the bike application
This now sends a set of lights to the dashboard from a pico. I had to
adjust some of the colors as they do not look nearly as good in lights
as they do in the screen. There is no real application loop yet, no the
ability to get feedback from external controls.
2024-07-08 09:29:32 -04:00
Savanni D'Gerinel 0b949111d2 Switch to a fixed point arithmatic library 2024-07-08 09:28:40 -04:00
Savanni D'Gerinel 6164cb3b39 Refactor the bike library until it compiles with no_std
Theoretically, this is the first step to getting to running on the pico
2024-07-08 09:28:40 -04:00
Savanni D'Gerinel 22f0f9061c Rotate the right side
The actual bike is going to be a long loop which folds from the end of
the left side to the back end of the right side. This requires that the
colors get moved around for proper mirroring.
2024-07-08 09:28:40 -04:00
Savanni D'Gerinel 0bb5e62f96 Set up a bunch of animations and some state transitions! 2024-07-08 09:28:40 -04:00
Savanni D'Gerinel 06aedc34bb Now I'm able to send messages from the UI to the core 2024-07-08 09:28:40 -04:00
Savanni D'Gerinel 84b077e20c Build the core of the application. 2024-07-08 09:28:40 -04:00
Savanni D'Gerinel fc2e88add2 Set up a GTK simulator for the bike lights engine 2024-07-08 09:28:38 -04:00
Savanni D'Gerinel 15c4ae9bad Update the review tree when navigating 2024-05-07 08:49:49 -04:00
Savanni D'Gerinel 7dd531b493 It is now possible to move backwards and forwards on the mainline of a tree 2024-05-07 07:53:15 -04:00
Savanni D'Gerinel cbfb3f2e37 Clean up tests 2024-05-01 09:36:48 -04:00
Savanni D'Gerinel 9540a2c5bb Highlight the current node and make all nodes a bit larger 2024-04-30 23:34:16 -04:00
Savanni D'Gerinel 6165d65977 Make the review tree scrollable 2024-04-30 23:28:12 -04:00
Savanni D'Gerinel 4f8a1636c1 Set up a view model for the game review and highlight current node 2024-04-30 23:27:05 -04:00
Savanni D'Gerinel 20b02fbd90 Update the Nix derivation 2024-04-30 22:26:12 -04:00
Savanni D'Gerinel 278ec27b4e Linting 2024-04-30 22:25:01 -04:00
Savanni D'Gerinel 8b7add37c1 Switch from slab_tree to nary_tree 2024-04-30 22:25:01 -04:00
Savanni D'Gerinel 5441a3c441 Adapt the app to the new slab tree 2024-04-30 22:25:01 -04:00
Savanni D'Gerinel b1374229f3 Calculate out the depth and width of each node 2024-04-30 22:25:01 -04:00
Savanni D'Gerinel bc5042c004 Start propogating the slab_tree up to OTG 2024-04-30 22:25:01 -04:00
Savanni D'Gerinel 0534143d6b Finish implementing GameTree.Clone and PartialEq 2024-04-30 22:25:01 -04:00
Savanni D'Gerinel d7f5269e15 Begin implementing traits for a game record 2024-04-30 22:25:01 -04:00
Savanni D'Gerinel c913e9da37 Convert the recursive parse tree to a slab GameTree 2024-04-30 22:25:01 -04:00
Savanni D'Gerinel c50bd652f1 Switch from my custom Tree setup to a slab tree in GameRecord 2024-04-30 22:25:01 -04:00
Savanni D'Gerinel 093e1f7f8a Start writing functions for finding a node within the tree 2024-04-30 22:25:01 -04:00
Savanni D'Gerinel 3c94f906a6 Update Cargo.nix 2024-04-19 12:27:17 -04:00
Savanni D'Gerinel 0aecaee760 Use pre-loaded images 2024-04-05 10:47:16 -04:00
Savanni D'Gerinel baeb458126 Create a resource manager that preloads the images 2024-04-05 10:06:52 -04:00
Savanni D'Gerinel da144a58ec Revise the graphics 2024-04-05 09:15:20 -04:00
Savanni D'Gerinel f09af67193 Add images for fancier stones 2024-04-01 00:14:15 -04:00
Savanni D'Gerinel 32391a46e7 Render the board in the background 2024-03-31 19:59:44 -04:00
Savanni D'Gerinel 0a62c96b0f Clippy 2024-03-31 19:36:44 -04:00
Savanni D'Gerinel 78863ee709 Cleanups 2024-03-31 18:16:41 -04:00
73 changed files with 22859 additions and 677 deletions

738
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -7917,6 +7917,28 @@ rec {
};
resolvedDefaultFeatures = [ "default" ];
};
"nary_tree" = rec {
crateName = "nary_tree";
version = "0.4.3";
edition = "2021";
sha256 = "1iqray1a716995l9mmvz5sfqrwg9a235bvrkpcn8bcqwjnwfv1pv";
authors = [
"Ian <iwburns8@gmail.com>"
"David Cohen <dacohen@pm.me>"
];
dependencies = [
{
name = "slab";
packageId = "slab";
}
{
name = "snowflake";
packageId = "snowflake";
}
];
features = {
};
};
"native-tls" = rec {
crateName = "native-tls";
version = "0.2.11";
@ -8551,6 +8573,10 @@ rec {
name = "grid";
packageId = "grid";
}
{
name = "nary_tree";
packageId = "nary_tree";
}
{
name = "serde";
packageId = "serde 1.0.193";
@ -8568,6 +8594,11 @@ rec {
name = "thiserror";
packageId = "thiserror";
}
{
name = "uuid";
packageId = "uuid 0.8.2";
features = [ "v4" "serde" ];
}
];
devDependencies = [
{
@ -8647,6 +8678,11 @@ rec {
packageId = "tokio";
features = [ "full" ];
}
{
name = "uuid";
packageId = "uuid 0.8.2";
features = [ "v4" "serde" ];
}
];
buildDependencies = [
{
@ -11560,6 +11596,10 @@ rec {
packageId = "chrono";
features = [ "serde" ];
}
{
name = "nary_tree";
packageId = "nary_tree";
}
{
name = "nom";
packageId = "nom";
@ -11793,6 +11833,21 @@ rec {
};
resolvedDefaultFeatures = [ "const_generics" "const_new" "union" ];
};
"snowflake" = rec {
crateName = "snowflake";
version = "1.3.0";
edition = "2015";
sha256 = "1wadr7bxdxbmkbqkqsvzan6q1h3mxqpxningi3ss3v9jaav7n817";
authors = [
"Steven Allen <steven@stebalien.com>"
];
features = {
"serde" = [ "dep:serde" ];
"serde_derive" = [ "dep:serde_derive" ];
"serde_support" = [ "serde" "serde_derive" ];
};
resolvedDefaultFeatures = [ "default" ];
};
"socket2 0.4.10" = rec {
crateName = "socket2";
version = "0.4.10";

View File

@ -2,6 +2,9 @@
resolver = "2"
members = [
"authdb",
"bike-lights/bike",
"bike-lights/core",
"bike-lights/simulator",
"changeset",
"config",
"config-derive",
@ -27,5 +30,5 @@ members = [
"sgf",
"timezone-testing",
"tree",
"visions/server",
"visions/server", "gm-dash/server",
]

View File

@ -0,0 +1,12 @@
[build]
target = "thumbv6m-none-eabi"
[target.thumbv6m-none-eabi]
rustflags = [
"-C", "link-arg=--nmagic",
"-C", "link-arg=-Tlink.x",
"-C", "inline-threshold=5",
"-C", "no-vectorize-loops",
]
runner = "elf2uf2-rs -d"

View File

@ -0,0 +1,18 @@
[package]
name = "bike"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
az = { version = "1" }
cortex-m-rt = { version = "0.7.3" }
cortex-m = { version = "0.7.7" }
embedded-alloc = { version = "0.5.1" }
embedded-hal = { version = "0.2.7" }
fixed = { version = "1" }
fugit = { version = "0.3.7" }
lights-core = { path = "../core" }
panic-halt = { version = "0.2.0" }
rp-pico = { version = "0.8.0" }

View File

@ -0,0 +1,241 @@
#![no_main]
#![no_std]
extern crate alloc;
use alloc::boxed::Box;
use az::*;
use core::cell::RefCell;
use cortex_m::delay::Delay;
use embedded_alloc::Heap;
use embedded_hal::{blocking::spi::Write, digital::v2::InputPin, digital::v2::OutputPin};
use fixed::types::I16F16;
use fugit::RateExtU32;
use lights_core::{App, BodyPattern, DashboardPattern, Event, Instant, FPS, UI};
use panic_halt as _;
use rp_pico::{
entry,
hal::{
clocks::init_clocks_and_plls,
gpio::{FunctionSio, Pin, PinId, PullDown, PullUp, SioInput, SioOutput},
pac::{CorePeripherals, Peripherals},
spi::{Enabled, Spi, SpiDevice, ValidSpiPinout},
watchdog::Watchdog,
Clock, Sio,
},
Pins,
};
#[global_allocator]
static HEAP: Heap = Heap::empty();
const LIGHT_SCALE: I16F16 = I16F16::lit("256.0");
const DASHBOARD_BRIGHTESS: u8 = 1;
const BODY_BRIGHTNESS: u8 = 8;
struct DebouncedButton<P: PinId> {
debounce: Instant,
pin: Pin<P, FunctionSio<SioInput>, PullUp>,
}
impl<P: PinId> DebouncedButton<P> {
fn new(pin: Pin<P, FunctionSio<SioInput>, PullUp>) -> Self {
Self {
debounce: Instant((0 as u32).into()),
pin,
}
}
fn is_low(&self, time: Instant) -> bool {
if time <= self.debounce {
return false;
}
self.pin.is_low().unwrap_or(false)
}
fn set_debounce(&mut self, time: Instant) {
self.debounce = time + Instant((250 as u32).into());
}
}
struct BikeUI<
D: SpiDevice,
P: ValidSpiPinout<D>,
LeftId: PinId,
RightId: PinId,
PreviousId: PinId,
NextId: PinId,
BrakeId: PinId,
> {
spi: RefCell<Spi<Enabled, D, P, 8>>,
left_blinker_button: DebouncedButton<LeftId>,
right_blinker_button: DebouncedButton<RightId>,
previous_animation_button: DebouncedButton<PreviousId>,
next_animation_button: DebouncedButton<NextId>,
brake_sensor: Pin<BrakeId, FunctionSio<SioInput>, PullUp>,
brake_enabled: bool,
}
impl<
D: SpiDevice,
P: ValidSpiPinout<D>,
LeftId: PinId,
RightId: PinId,
PreviousId: PinId,
NextId: PinId,
BrakeId: PinId,
> BikeUI<D, P, LeftId, RightId, PreviousId, NextId, BrakeId>
{
fn new(
spi: Spi<Enabled, D, P, 8>,
left_blinker_button: Pin<LeftId, FunctionSio<SioInput>, PullUp>,
right_blinker_button: Pin<RightId, FunctionSio<SioInput>, PullUp>,
previous_animation_button: Pin<PreviousId, FunctionSio<SioInput>, PullUp>,
next_animation_button: Pin<NextId, FunctionSio<SioInput>, PullUp>,
brake_sensor: Pin<BrakeId, FunctionSio<SioInput>, PullUp>,
) -> Self {
Self {
spi: RefCell::new(spi),
left_blinker_button: DebouncedButton::new(left_blinker_button),
right_blinker_button: DebouncedButton::new(right_blinker_button),
previous_animation_button: DebouncedButton::new(previous_animation_button),
next_animation_button: DebouncedButton::new(next_animation_button),
brake_sensor,
brake_enabled: false,
}
}
}
impl<
D: SpiDevice,
P: ValidSpiPinout<D>,
LeftId: PinId,
RightId: PinId,
PreviousId: PinId,
NextId: PinId,
BrakeId: PinId,
> UI for BikeUI<D, P, LeftId, RightId, PreviousId, NextId, BrakeId>
{
fn check_event(&mut self, current_time: Instant) -> Option<Event> {
if self.brake_sensor.is_high().unwrap_or(true) && !self.brake_enabled {
self.brake_enabled = true;
Some(Event::Brake)
} else if self.brake_sensor.is_low().unwrap_or(false) && self.brake_enabled {
self.brake_enabled = false;
Some(Event::BrakeRelease)
} else if self.left_blinker_button.is_low(current_time) {
self.left_blinker_button.set_debounce(current_time);
Some(Event::LeftBlinker)
} else if self.right_blinker_button.is_low(current_time) {
self.right_blinker_button.set_debounce(current_time);
Some(Event::RightBlinker)
} else if self.previous_animation_button.is_low(current_time) {
self.previous_animation_button.set_debounce(current_time);
Some(Event::PreviousPattern)
} else if self.next_animation_button.is_low(current_time) {
self.next_animation_button.set_debounce(current_time);
Some(Event::NextPattern)
} else {
None
}
}
fn update_lights(&self, dashboard_lights: DashboardPattern, body_lights: BodyPattern) {
let mut lights: [u8; 260] = [0; 260];
lights[256] = 0xff;
lights[257] = 0xff;
lights[258] = 0xff;
lights[259] = 0xff;
for (idx, rgb) in dashboard_lights.iter().enumerate() {
lights[(idx + 1) * 4 + 0] = 0xe0 + DASHBOARD_BRIGHTESS;
lights[(idx + 1) * 4 + 1] = (I16F16::from(rgb.r) * LIGHT_SCALE).saturating_as();
lights[(idx + 1) * 4 + 2] = (I16F16::from(rgb.b) * LIGHT_SCALE).saturating_as();
lights[(idx + 1) * 4 + 3] = (I16F16::from(rgb.g) * LIGHT_SCALE).saturating_as();
}
for (idx, rgb) in body_lights.iter().enumerate() {
lights[(idx + 4) * 4 + 0] = 0xe0 + BODY_BRIGHTNESS;
lights[(idx + 4) * 4 + 1] = (I16F16::from(rgb.b) * LIGHT_SCALE).saturating_as();
lights[(idx + 4) * 4 + 2] = (I16F16::from(rgb.g) * LIGHT_SCALE).saturating_as();
lights[(idx + 4) * 4 + 3] = (I16F16::from(rgb.r) * LIGHT_SCALE).saturating_as();
}
let mut spi = self.spi.borrow_mut();
spi.write(lights.as_slice());
}
}
#[entry]
fn main() -> ! {
{
use core::mem::MaybeUninit;
const HEAP_SIZE: usize = 8096;
static mut HEAP_MEM: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) }
}
let mut pac = Peripherals::take().unwrap();
let core = CorePeripherals::take().unwrap();
let sio = Sio::new(pac.SIO);
let mut watchdog = Watchdog::new(pac.WATCHDOG);
let pins = Pins::new(
pac.IO_BANK0,
pac.PADS_BANK0,
sio.gpio_bank0,
&mut pac.RESETS,
);
let clocks = init_clocks_and_plls(
12_000_000u32,
pac.XOSC,
pac.CLOCKS,
pac.PLL_SYS,
pac.PLL_USB,
&mut pac.RESETS,
&mut watchdog,
)
.ok()
.unwrap();
let mut delay = Delay::new(core.SYST, clocks.system_clock.freq().to_Hz());
let mut spi_clk = pins.gpio10.into_function();
let mut spi_sdo = pins.gpio11.into_function();
let spi = Spi::<_, _, _, 8>::new(pac.SPI1, (spi_sdo, spi_clk));
let mut spi = spi.init(
&mut pac.RESETS,
clocks.peripheral_clock.freq(),
1_u32.MHz(),
embedded_hal::spi::MODE_1,
);
let left_blinker_button = pins.gpio16.into_pull_up_input();
let right_blinker_button = pins.gpio17.into_pull_up_input();
let previous_animation_button = pins.gpio27.into_pull_up_input();
let next_animation_button = pins.gpio26.into_pull_up_input();
let brake_sensor = pins.gpio18.into_pull_up_input();
let mut led_pin = pins.led.into_push_pull_output();
let ui = BikeUI::new(
spi,
left_blinker_button,
right_blinker_button,
previous_animation_button,
next_animation_button,
brake_sensor,
);
let mut app = App::new(Box::new(ui));
led_pin.set_high();
let mut time = Instant::default();
let delay_ms = 1000 / (FPS as u32);
loop {
app.tick(time);
delay.delay_ms(delay_ms);
time = time + Instant(delay_ms.into());
}
}

View File

@ -0,0 +1,10 @@
[package]
name = "lights-core"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
az = { version = "1" }
fixed = { version = "1" }

481
bike-lights/core/src/lib.rs Normal file
View File

@ -0,0 +1,481 @@
#![no_std]
extern crate alloc;
use alloc::boxed::Box;
use az::*;
use core::{
clone::Clone,
cmp::PartialEq,
default::Default,
ops::{Add, Sub},
option::Option,
};
use fixed::types::{I48F16, I8F8, U128F0, U16F0};
mod patterns;
pub use patterns::*;
mod types;
pub use types::{BodyPattern, DashboardPattern, RGB};
fn calculate_frames(starting_time: U128F0, now: U128F0) -> U16F0 {
let frames_128 = (now - starting_time) / U128F0::from(FPS);
(frames_128 % U128F0::from(U16F0::MAX)).cast()
}
fn calculate_slope(start: I8F8, end: I8F8, frames: U16F0) -> I8F8 {
let slope_i16f16 = (I48F16::from(end) - I48F16::from(start)) / I48F16::from(frames);
slope_i16f16.saturating_as()
}
fn linear_ease(value: I8F8, frames: U16F0, slope: I8F8) -> I8F8 {
let value_i16f16 = I48F16::from(value) + I48F16::from(frames) * I48F16::from(slope);
value_i16f16.saturating_as()
}
#[derive(Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
pub struct Instant(pub U128F0);
impl Default for Instant {
fn default() -> Self {
Self(U128F0::from(0 as u8))
}
}
impl Add for Instant {
type Output = Self;
fn add(self, r: Self) -> Self::Output {
Self(self.0 + r.0)
}
}
impl Sub for Instant {
type Output = Self;
fn sub(self, r: Self) -> Self::Output {
Self(self.0 - r.0)
}
}
pub const FPS: u8 = 30;
pub trait UI {
fn check_event(&mut self, current_time: Instant) -> Option<Event>;
fn update_lights(&self, dashboard_lights: DashboardPattern, body_lights: BodyPattern);
}
pub trait Animation {
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern);
}
/*
pub struct DefaultAnimation {}
impl Animation for DefaultAnimation {
fn tick(&mut self, _: Instant) -> (DashboardPattern, BodyPattern) {
(WATER_DASHBOARD, WATER_BODY)
}
}
*/
pub struct Fade {
starting_dashboard: DashboardPattern,
starting_lights: BodyPattern,
start_time: Instant,
dashboard_slope: [RGB<I8F8>; 3],
body_slope: [RGB<I8F8>; 60],
frames: U16F0,
}
impl Fade {
fn new(
dashboard: DashboardPattern,
lights: BodyPattern,
ending_dashboard: DashboardPattern,
ending_lights: BodyPattern,
frames: U16F0,
time: Instant,
) -> Self {
let mut dashboard_slope = [Default::default(); 3];
let mut body_slope = [Default::default(); 60];
for i in 0..3 {
let slope = RGB {
r: calculate_slope(dashboard[i].r, ending_dashboard[i].r, frames),
g: calculate_slope(dashboard[i].g, ending_dashboard[i].g, frames),
b: calculate_slope(dashboard[i].b, ending_dashboard[i].b, frames),
};
dashboard_slope[i] = slope;
}
for i in 0..60 {
let slope = RGB {
r: calculate_slope(lights[i].r, ending_lights[i].r, frames),
g: calculate_slope(lights[i].g, ending_lights[i].g, frames),
b: calculate_slope(lights[i].b, ending_lights[i].b, frames),
};
body_slope[i] = slope;
}
Self {
starting_dashboard: dashboard,
starting_lights: lights,
start_time: time,
dashboard_slope,
body_slope,
frames,
}
}
}
impl Animation for Fade {
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern) {
let mut frames = calculate_frames(self.start_time.0, time.0);
if frames > self.frames {
frames = self.frames
}
let mut dashboard_pattern: DashboardPattern = OFF_DASHBOARD;
let mut body_pattern: BodyPattern = OFF_BODY;
for i in 0..3 {
dashboard_pattern[i].r = linear_ease(
self.starting_dashboard[i].r,
frames,
self.dashboard_slope[i].r,
);
dashboard_pattern[i].g = linear_ease(
self.starting_dashboard[i].g,
frames,
self.dashboard_slope[i].g,
);
dashboard_pattern[i].b = linear_ease(
self.starting_dashboard[i].b,
frames,
self.dashboard_slope[i].b,
);
}
for i in 0..60 {
body_pattern[i].r =
linear_ease(self.starting_lights[i].r, frames, self.body_slope[i].r);
body_pattern[i].g =
linear_ease(self.starting_lights[i].g, frames, self.body_slope[i].g);
body_pattern[i].b =
linear_ease(self.starting_lights[i].b, frames, self.body_slope[i].b);
}
(dashboard_pattern, body_pattern)
}
}
#[derive(Debug)]
pub enum FadeDirection {
Transition,
FadeIn,
FadeOut,
}
pub enum BlinkerDirection {
Left,
Right,
}
pub struct Blinker {
transition: Fade,
fade_in: Fade,
fade_out: Fade,
direction: FadeDirection,
start_time: Instant,
frames: U16F0,
}
impl Blinker {
fn new(
starting_dashboard: DashboardPattern,
starting_body: BodyPattern,
direction: BlinkerDirection,
time: Instant,
) -> Self {
let mut ending_dashboard = OFF_DASHBOARD.clone();
match direction {
BlinkerDirection::Left => {
ending_dashboard[0].r = LEFT_BLINKER_DASHBOARD[0].r;
ending_dashboard[0].g = LEFT_BLINKER_DASHBOARD[0].g;
ending_dashboard[0].b = LEFT_BLINKER_DASHBOARD[0].b;
}
BlinkerDirection::Right => {
ending_dashboard[2].r = RIGHT_BLINKER_DASHBOARD[2].r;
ending_dashboard[2].g = RIGHT_BLINKER_DASHBOARD[2].g;
ending_dashboard[2].b = RIGHT_BLINKER_DASHBOARD[2].b;
}
}
let mut ending_body = OFF_BODY.clone();
match direction {
BlinkerDirection::Left => {
for i in 0..30 {
ending_body[i].r = LEFT_BLINKER_BODY[i].r;
ending_body[i].g = LEFT_BLINKER_BODY[i].g;
ending_body[i].b = LEFT_BLINKER_BODY[i].b;
}
}
BlinkerDirection::Right => {
for i in 30..60 {
ending_body[i].r = RIGHT_BLINKER_BODY[i].r;
ending_body[i].g = RIGHT_BLINKER_BODY[i].g;
ending_body[i].b = RIGHT_BLINKER_BODY[i].b;
}
}
}
Blinker {
transition: Fade::new(
starting_dashboard.clone(),
starting_body.clone(),
ending_dashboard.clone(),
ending_body.clone(),
BLINKER_FRAMES,
time,
),
fade_in: Fade::new(
OFF_DASHBOARD.clone(),
OFF_BODY.clone(),
ending_dashboard.clone(),
ending_body.clone(),
BLINKER_FRAMES,
time,
),
fade_out: Fade::new(
ending_dashboard.clone(),
ending_body.clone(),
OFF_DASHBOARD.clone(),
OFF_BODY.clone(),
BLINKER_FRAMES,
time,
),
direction: FadeDirection::Transition,
start_time: time,
frames: BLINKER_FRAMES,
}
}
}
impl Animation for Blinker {
fn tick(&mut self, time: Instant) -> (DashboardPattern, BodyPattern) {
let frames = calculate_frames(self.start_time.0, time.0);
if frames > self.frames {
match self.direction {
FadeDirection::Transition => {
self.direction = FadeDirection::FadeOut;
self.fade_out.start_time = time;
}
FadeDirection::FadeIn => {
self.direction = FadeDirection::FadeOut;
self.fade_out.start_time = time;
}
FadeDirection::FadeOut => {
self.direction = FadeDirection::FadeIn;
self.fade_in.start_time = time;
}
}
self.start_time = time;
}
match self.direction {
FadeDirection::Transition => self.transition.tick(time),
FadeDirection::FadeIn => self.fade_in.tick(time),
FadeDirection::FadeOut => self.fade_out.tick(time),
}
}
}
#[derive(Clone, Debug)]
pub enum Event {
Brake,
BrakeRelease,
LeftBlinker,
NextPattern,
PreviousPattern,
RightBlinker,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Pattern {
Water,
GayPride,
TransPride,
}
impl Pattern {
fn previous(&self) -> Pattern {
match self {
Pattern::Water => Pattern::TransPride,
Pattern::GayPride => Pattern::Water,
Pattern::TransPride => Pattern::GayPride,
}
}
fn next(&self) -> Pattern {
match self {
Pattern::Water => Pattern::GayPride,
Pattern::GayPride => Pattern::TransPride,
Pattern::TransPride => Pattern::Water,
}
}
fn dashboard(&self) -> DashboardPattern {
match self {
Pattern::Water => WATER_DASHBOARD,
Pattern::GayPride => PRIDE_DASHBOARD,
Pattern::TransPride => TRANS_PRIDE_DASHBOARD,
}
}
fn body(&self) -> BodyPattern {
match self {
Pattern::Water => OFF_BODY,
Pattern::GayPride => PRIDE_BODY,
Pattern::TransPride => TRANS_PRIDE_BODY,
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum State {
Pattern(Pattern),
Brake,
LeftBlinker,
RightBlinker,
BrakeLeftBlinker,
BrakeRightBlinker,
}
pub struct App {
ui: Box<dyn UI>,
state: State,
home_pattern: Pattern,
current_animation: Box<dyn Animation>,
dashboard_lights: DashboardPattern,
lights: BodyPattern,
}
impl App {
pub fn new(ui: Box<dyn UI>) -> Self {
let pattern = Pattern::Water;
Self {
ui,
state: State::Pattern(pattern),
home_pattern: pattern,
current_animation: Box::new(Fade::new(
OFF_DASHBOARD,
OFF_BODY,
pattern.dashboard(),
pattern.body(),
DEFAULT_FRAMES,
Instant((0 as u32).into()),
)),
dashboard_lights: OFF_DASHBOARD,
lights: OFF_BODY,
}
}
fn update_animation(&mut self, time: Instant) {
match self.state {
State::Pattern(ref pattern) => {
self.current_animation = Box::new(Fade::new(
self.dashboard_lights.clone(),
self.lights.clone(),
pattern.dashboard(),
pattern.body(),
DEFAULT_FRAMES,
time,
))
}
State::Brake => {
self.current_animation = Box::new(Fade::new(
self.dashboard_lights.clone(),
self.lights.clone(),
BRAKES_DASHBOARD,
BRAKES_BODY,
BRAKES_FRAMES,
time,
));
}
State::LeftBlinker => {
self.current_animation = Box::new(Blinker::new(
self.dashboard_lights.clone(),
self.lights.clone(),
BlinkerDirection::Left,
time,
));
}
State::RightBlinker => {
self.current_animation = Box::new(Blinker::new(
self.dashboard_lights.clone(),
self.lights.clone(),
BlinkerDirection::Right,
time,
));
}
State::BrakeLeftBlinker => (),
State::BrakeRightBlinker => (),
}
}
fn update_state(&mut self, event: Event) {
match event {
Event::Brake => {
if self.state == State::Brake {
self.state = State::Pattern(self.home_pattern);
} else {
self.state = State::Brake;
}
}
Event::BrakeRelease => self.state = State::Pattern(self.home_pattern),
Event::LeftBlinker => match self.state {
State::Brake => self.state = State::BrakeLeftBlinker,
State::BrakeLeftBlinker => self.state = State::Brake,
State::LeftBlinker => self.state = State::Pattern(self.home_pattern),
_ => self.state = State::LeftBlinker,
},
Event::NextPattern => match self.state {
State::Pattern(ref pattern) => {
self.home_pattern = pattern.next();
self.state = State::Pattern(self.home_pattern);
}
_ => (),
},
Event::PreviousPattern => match self.state {
State::Pattern(ref pattern) => {
self.home_pattern = pattern.previous();
self.state = State::Pattern(self.home_pattern);
}
_ => (),
},
Event::RightBlinker => match self.state {
State::Brake => self.state = State::BrakeRightBlinker,
State::BrakeRightBlinker => self.state = State::Brake,
State::RightBlinker => self.state = State::Pattern(self.home_pattern),
_ => self.state = State::RightBlinker,
},
}
}
pub fn tick(&mut self, time: Instant) {
match self.ui.check_event(time) {
Some(event) => {
self.update_state(event);
self.update_animation(time);
}
None => {}
};
let (dashboard, lights) = self.current_animation.tick(time);
self.dashboard_lights = dashboard.clone();
self.lights = lights.clone();
self.ui.update_lights(dashboard, lights);
}
}

View File

@ -0,0 +1,333 @@
use crate::{BodyPattern, DashboardPattern, RGB};
use fixed::types::{I8F8, U16F0};
pub const RGB_OFF: RGB<I8F8> = RGB {
r: I8F8::lit("0"),
g: I8F8::lit("0"),
b: I8F8::lit("0"),
};
pub const RGB_WHITE: RGB<I8F8> = RGB {
r: I8F8::lit("1"),
g: I8F8::lit("1"),
b: I8F8::lit("1"),
};
pub const BRAKES_RED: RGB<I8F8> = RGB {
r: I8F8::lit("1"),
g: I8F8::lit("0"),
b: I8F8::lit("0"),
};
pub const BLINKER_AMBER: RGB<I8F8> = RGB {
r: I8F8::lit("1"),
g: I8F8::lit("0.15"),
b: I8F8::lit("0"),
};
pub const PRIDE_RED: RGB<I8F8> = RGB {
r: I8F8::lit("0.95"),
g: I8F8::lit("0.00"),
b: I8F8::lit("0.00"),
};
pub const PRIDE_ORANGE: RGB<I8F8> = RGB {
r: I8F8::lit("1.0"),
g: I8F8::lit("0.25"),
b: I8F8::lit("0"),
};
pub const PRIDE_YELLOW: RGB<I8F8> = RGB {
r: I8F8::lit("1.0"),
g: I8F8::lit("0.85"),
b: I8F8::lit("0"),
};
pub const PRIDE_GREEN: RGB<I8F8> = RGB {
r: I8F8::lit("0"),
g: I8F8::lit("0.95"),
b: I8F8::lit("0.05"),
};
pub const PRIDE_INDIGO: RGB<I8F8> = RGB {
r: I8F8::lit("0.04"),
g: I8F8::lit("0.15"),
b: I8F8::lit("0.55"),
};
pub const PRIDE_VIOLET: RGB<I8F8> = RGB {
r: I8F8::lit("0.75"),
g: I8F8::lit("0.0"),
b: I8F8::lit("0.80"),
};
pub const TRANS_BLUE: RGB<I8F8> = RGB {
r: I8F8::lit("0.06"),
g: I8F8::lit("0.41"),
b: I8F8::lit("0.98"),
};
pub const TRANS_PINK: RGB<I8F8> = RGB {
r: I8F8::lit("0.96"),
g: I8F8::lit("0.16"),
b: I8F8::lit("0.32"),
};
pub const WATER_1: RGB<I8F8> = RGB {
r: I8F8::lit("0.0"),
g: I8F8::lit("0.0"),
b: I8F8::lit("0.75"),
};
pub const WATER_2: RGB<I8F8> = RGB {
r: I8F8::lit("0.8"),
g: I8F8::lit("0.8"),
b: I8F8::lit("0.8"),
};
pub const WATER_3: RGB<I8F8> = RGB {
r: I8F8::lit("0.00"),
g: I8F8::lit("0.75"),
b: I8F8::lit("0.75"),
};
pub const OFF_DASHBOARD: DashboardPattern = [RGB_OFF; 3];
pub const OFF_BODY: BodyPattern = [RGB_OFF; 60];
pub const DEFAULT_FRAMES: U16F0 = U16F0::lit("30");
pub const WATER_DASHBOARD: DashboardPattern = [WATER_1, WATER_2, WATER_3];
pub const WATER_BODY: BodyPattern = [RGB_OFF; 60];
pub const PRIDE_DASHBOARD: DashboardPattern = [PRIDE_RED, PRIDE_GREEN, PRIDE_INDIGO];
pub const PRIDE_BODY: BodyPattern = [
// Left Side
// Red
PRIDE_RED,
PRIDE_RED,
PRIDE_RED,
PRIDE_RED,
PRIDE_RED,
// Orange
PRIDE_ORANGE,
PRIDE_ORANGE,
PRIDE_ORANGE,
PRIDE_ORANGE,
PRIDE_ORANGE,
// Yellow
PRIDE_YELLOW,
PRIDE_YELLOW,
PRIDE_YELLOW,
PRIDE_YELLOW,
PRIDE_YELLOW,
// Green
PRIDE_GREEN,
PRIDE_GREEN,
PRIDE_GREEN,
PRIDE_GREEN,
PRIDE_GREEN,
// Indigo
PRIDE_INDIGO,
PRIDE_INDIGO,
PRIDE_INDIGO,
PRIDE_INDIGO,
PRIDE_INDIGO,
// Violet
PRIDE_VIOLET,
PRIDE_VIOLET,
PRIDE_VIOLET,
PRIDE_VIOLET,
PRIDE_VIOLET,
// Right Side
// Violet
PRIDE_VIOLET,
PRIDE_VIOLET,
PRIDE_VIOLET,
PRIDE_VIOLET,
PRIDE_VIOLET,
// Indigo
PRIDE_INDIGO,
PRIDE_INDIGO,
PRIDE_INDIGO,
PRIDE_INDIGO,
PRIDE_INDIGO,
// Green
PRIDE_GREEN,
PRIDE_GREEN,
PRIDE_GREEN,
PRIDE_GREEN,
PRIDE_GREEN,
// Yellow
PRIDE_YELLOW,
PRIDE_YELLOW,
PRIDE_YELLOW,
PRIDE_YELLOW,
PRIDE_YELLOW,
// Orange
PRIDE_ORANGE,
PRIDE_ORANGE,
PRIDE_ORANGE,
PRIDE_ORANGE,
PRIDE_ORANGE,
// Red
PRIDE_RED,
PRIDE_RED,
PRIDE_RED,
PRIDE_RED,
PRIDE_RED,
];
pub const TRANS_PRIDE_DASHBOARD: DashboardPattern = [TRANS_BLUE, RGB_WHITE, TRANS_PINK];
pub const TRANS_PRIDE_BODY: BodyPattern = [
// Left Side
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_PINK, TRANS_PINK,
TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, RGB_WHITE, RGB_WHITE, RGB_WHITE, RGB_WHITE,
RGB_WHITE, RGB_WHITE, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK,
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE,
// Right side
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_PINK, TRANS_PINK,
TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, RGB_WHITE, RGB_WHITE, RGB_WHITE, RGB_WHITE,
RGB_WHITE, RGB_WHITE, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK, TRANS_PINK,
TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE, TRANS_BLUE,
];
pub const BRAKES_FRAMES: U16F0 = U16F0::lit("15");
pub const BRAKES_DASHBOARD: DashboardPattern = [BRAKES_RED; 3];
pub const BRAKES_BODY: BodyPattern = [BRAKES_RED; 60];
pub const BLINKER_FRAMES: U16F0 = U16F0::lit("10");
pub const LEFT_BLINKER_DASHBOARD: DashboardPattern = [BLINKER_AMBER, RGB_OFF, RGB_OFF];
pub const LEFT_BLINKER_BODY: BodyPattern = [
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
];
pub const RIGHT_BLINKER_DASHBOARD: DashboardPattern = [RGB_OFF, RGB_OFF, BLINKER_AMBER];
pub const RIGHT_BLINKER_BODY: BodyPattern = [
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
RGB_OFF,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
BLINKER_AMBER,
];

View File

@ -0,0 +1,17 @@
use core::default::Default;
use fixed::types::I8F8;
#[derive(Clone, Copy, Default, Debug)]
pub struct RGB<T> {
pub r: T,
pub g: T,
pub b: T,
}
const DASHBOARD_LIGHT_COUNT: usize = 3;
pub type DashboardPattern = [RGB<I8F8>; DASHBOARD_LIGHT_COUNT];
const BODY_LIGHT_COUNT: usize = 60;
pub type BodyPattern = [RGB<I8F8>; BODY_LIGHT_COUNT];

View File

@ -0,0 +1,16 @@
[package]
name = "simulator"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
cairo-rs = { version = "0.18" }
fixed = { version = "1" }
gio = { version = "0.18" }
glib = { version = "0.18" }
gtk = { version = "0.7", package = "gtk4", features = [ "v4_8" ] }
lights-core = { path = "../core" }
pango = { version = "*" }

View File

@ -0,0 +1,288 @@
use adw::prelude::*;
use fixed::types::{I8F8, U128F0};
use glib::{Object, Sender};
use gtk::subclass::prelude::*;
use lights_core::{
App, BodyPattern, DashboardPattern, Event, Instant, FPS, OFF_BODY, OFF_DASHBOARD, RGB, UI,
};
use std::{
cell::RefCell,
env,
rc::Rc,
sync::mpsc::{Receiver, TryRecvError},
};
const WIDTH: i32 = 640;
const HEIGHT: i32 = 480;
pub struct Update {
dashboard: DashboardPattern,
lights: BodyPattern,
}
pub struct DashboardLightsPrivate {
lights: Rc<RefCell<DashboardPattern>>,
}
#[glib::object_subclass]
impl ObjectSubclass for DashboardLightsPrivate {
const NAME: &'static str = "DashboardLights";
type Type = DashboardLights;
type ParentType = gtk::DrawingArea;
fn new() -> Self {
Self {
lights: Rc::new(RefCell::new(OFF_DASHBOARD)),
}
}
}
impl ObjectImpl for DashboardLightsPrivate {}
impl WidgetImpl for DashboardLightsPrivate {}
impl DrawingAreaImpl for DashboardLightsPrivate {}
glib::wrapper! {
pub struct DashboardLights(ObjectSubclass<DashboardLightsPrivate>) @extends gtk::DrawingArea, gtk::Widget;
}
impl DashboardLights {
pub fn new() -> Self {
let s: Self = Object::builder().build();
s.set_width_request(WIDTH);
s.set_height_request(100);
s.set_draw_func({
let s = s.clone();
move |_, context, width, _| {
let start = width as f64 / 2. - 150.;
let lights = s.imp().lights.borrow();
for i in 0..3 {
context.set_source_rgb(
lights[i].r.into(),
lights[i].g.into(),
lights[i].b.into(),
);
context.rectangle(start + 100. * i as f64, 10., 80., 80.);
let _ = context.fill();
}
}
});
s
}
pub fn set_lights(&self, lights: DashboardPattern) {
*self.imp().lights.borrow_mut() = lights;
self.queue_draw();
}
}
pub struct BikeLightsPrivate {
lights: Rc<RefCell<BodyPattern>>,
}
#[glib::object_subclass]
impl ObjectSubclass for BikeLightsPrivate {
const NAME: &'static str = "BikeLights";
type Type = BikeLights;
type ParentType = gtk::DrawingArea;
fn new() -> Self {
Self {
lights: Rc::new(RefCell::new(OFF_BODY)),
}
}
}
impl ObjectImpl for BikeLightsPrivate {}
impl WidgetImpl for BikeLightsPrivate {}
impl DrawingAreaImpl for BikeLightsPrivate {}
glib::wrapper! {
pub struct BikeLights(ObjectSubclass<BikeLightsPrivate>) @extends gtk::DrawingArea, gtk::Widget;
}
impl BikeLights {
pub fn new() -> Self {
let s: Self = Object::builder().build();
s.set_width_request(WIDTH);
s.set_height_request(640);
let center = WIDTH as f64 / 2.;
s.set_draw_func({
let s = s.clone();
move |_, context, _, _| {
let lights = s.imp().lights.borrow();
for i in 0..30 {
context.set_source_rgb(
lights[i].r.into(),
lights[i].g.into(),
lights[i].b.into(),
);
context.rectangle(center - 45., 5. + 20. * i as f64, 15., 15.);
let _ = context.fill();
}
for i in 0..30 {
context.set_source_rgb(
lights[i + 30].r.into(),
lights[i + 30].g.into(),
lights[i + 30].b.into(),
);
context.rectangle(center + 15., 5. + 20. * (30. - (i + 1) as f64), 15., 15.);
let _ = context.fill();
}
}
});
s
}
pub fn set_lights(&self, lights: [RGB<I8F8>; 60]) {
*self.imp().lights.borrow_mut() = lights;
self.queue_draw();
}
}
struct GTKUI {
tx: Sender<Update>,
rx: Receiver<Event>,
}
impl UI for GTKUI {
fn check_event(&mut self, _: Instant) -> Option<Event> {
match self.rx.try_recv() {
Ok(event) => Some(event),
Err(TryRecvError::Empty) => None,
Err(TryRecvError::Disconnected) => None,
}
}
fn update_lights(&self, dashboard_lights: DashboardPattern, lights: BodyPattern) {
self.tx
.send(Update {
dashboard: dashboard_lights,
lights,
})
.unwrap();
}
}
fn main() {
let adw_app = adw::Application::builder()
.application_id("com.luminescent-dreams.bike-light-simulator")
.build();
adw_app.connect_activate(move |adw_app| {
let (update_tx, update_rx) =
gtk::glib::MainContext::channel::<Update>(gtk::glib::Priority::DEFAULT);
let (event_tx, event_rx) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut bike_app = App::new(Box::new(GTKUI {
tx: update_tx,
rx: event_rx,
}));
loop {
bike_app.tick(Instant(U128F0::from(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis(),
)));
std::thread::sleep(std::time::Duration::from_millis(1000 / (FPS as u64)));
}
});
let window = adw::ApplicationWindow::builder()
.application(adw_app)
.default_width(WIDTH)
.default_height(HEIGHT)
.build();
let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
let controls = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
let dashboard_lights = DashboardLights::new();
let bike_lights = BikeLights::new();
let left_button = gtk::Button::builder().label("L").build();
let brake_button = gtk::Button::builder().label("Brakes").build();
let right_button = gtk::Button::builder().label("R").build();
left_button.connect_clicked({
let event_tx = event_tx.clone();
move |_| {
let _ = event_tx.send(Event::LeftBlinker);
}
});
brake_button.connect_clicked({
let event_tx = event_tx.clone();
move |_| {
let _ = event_tx.send(Event::Brake);
}
});
right_button.connect_clicked({
let event_tx = event_tx.clone();
move |_| {
let _ = event_tx.send(Event::RightBlinker);
}
});
controls.append(&left_button);
controls.append(&brake_button);
controls.append(&right_button);
layout.append(&controls);
let pattern_controls = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.build();
let previous_pattern = gtk::Button::builder().label("Previous").build();
let next_pattern = gtk::Button::builder().label("Next").build();
previous_pattern.connect_clicked({
let event_tx = event_tx.clone();
move |_| {
let _ = event_tx.send(Event::PreviousPattern);
}
});
next_pattern.connect_clicked({
let event_tx = event_tx.clone();
move |_| {
let _ = event_tx.send(Event::NextPattern);
}
});
pattern_controls.append(&previous_pattern);
pattern_controls.append(&next_pattern);
layout.append(&pattern_controls);
layout.append(&dashboard_lights);
layout.append(&bike_lights);
update_rx.attach(None, {
let dashboard_lights = dashboard_lights.clone();
let bike_lights = bike_lights.clone();
move |Update { dashboard, lights }| {
dashboard_lights.set_lights(dashboard);
bike_lights.set_lights(lights);
glib::ControlFlow::Continue
}
});
window.set_content(Some(&layout));
window.present();
});
let args: Vec<String> = env::args().collect();
ApplicationExtManual::run_with_args(&adw_app, &args);
}

474
crate-hashes.json Normal file
View File

@ -0,0 +1,474 @@
{
"registry+https://github.com/rust-lang/crates.io-index#addr2line@0.21.0": "1jx0k3iwyqr8klqbzk6kjvr496yd94aspis10vwsj5wy7gib4c4a",
"registry+https://github.com/rust-lang/crates.io-index#adler32@1.2.0": "0d7jq7jsjyhsgbhnfq5fvrlh9j0i9g1fqrl2735ibv5f75yjgqda",
"registry+https://github.com/rust-lang/crates.io-index#adler@1.0.2": "1zim79cvzd5yrkzl3nyfx0avijwgk9fqv3yrscdy1cc79ih02qpj",
"registry+https://github.com/rust-lang/crates.io-index#ahash@0.8.6": "0yn9i8nc6mmv28ig9w3dga571q09vg9f1f650mi5z8phx42r6hli",
"registry+https://github.com/rust-lang/crates.io-index#aho-corasick@1.1.2": "1w510wnixvlgimkx1zjbvlxh6xps2vjgfqgwf5a6adlbjp5rv5mj",
"registry+https://github.com/rust-lang/crates.io-index#allocator-api2@0.2.16": "1iayppgq4wqbfbfcqmsbwgamj0s65012sskfvyx07pxavk3gyhh9",
"registry+https://github.com/rust-lang/crates.io-index#android-tzdata@0.1.1": "1w7ynjxrfs97xg3qlcdns4kgfpwcdv824g611fq32cag4cdr96g9",
"registry+https://github.com/rust-lang/crates.io-index#android_system_properties@0.1.5": "04b3wrz12837j7mdczqd95b732gw5q7q66cv4yn4646lvccp57l1",
"registry+https://github.com/rust-lang/crates.io-index#anstream@0.6.5": "1dm1mdbs1x6y3m3pz0qlamgiskb50i4q859676kx0pz8r8pajr6n",
"registry+https://github.com/rust-lang/crates.io-index#anstyle-parse@0.2.3": "134jhzrz89labrdwxxnjxqjdg06qvaflj1wkfnmyapwyldfwcnn7",
"registry+https://github.com/rust-lang/crates.io-index#anstyle-query@1.0.2": "0j3na4b1nma39g4x7cwvj009awxckjf3z2vkwhldgka44hqj72g2",
"registry+https://github.com/rust-lang/crates.io-index#anstyle-wincon@3.0.2": "19v0fv400bmp4niqpzxnhg83vz12mmqv7l2l8vi80qcdxj0lpm8w",
"registry+https://github.com/rust-lang/crates.io-index#anstyle@1.0.4": "11yxw02b6parn29s757z96rgiqbn8qy0fk9a3p3bhczm85dhfybh",
"registry+https://github.com/rust-lang/crates.io-index#anyhow@1.0.75": "1rmcjkim91c5mw7h9wn8nv0k6x118yz0xg0z1q18svgn42mqqrm4",
"registry+https://github.com/rust-lang/crates.io-index#async-channel@1.9.0": "0dbdlkzlncbibd3ij6y6jmvjd0cmdn48ydcfdpfhw09njd93r5c1",
"registry+https://github.com/rust-lang/crates.io-index#async-channel@2.1.1": "1337ywc1paw03rdlwh100kh8pa0zyp0nrlya8bpsn6zdqi5kz8qw",
"registry+https://github.com/rust-lang/crates.io-index#async-executor@1.8.0": "0z7rpayidhdqs4sdzjhh26z5155c1n94fycqni9793n4zjz5xbhp",
"registry+https://github.com/rust-lang/crates.io-index#async-global-executor@2.4.1": "1762s45cc134d38rrv0hyp41hv4iv6nmx59vswid2p0il8rvdc85",
"registry+https://github.com/rust-lang/crates.io-index#async-io@1.13.0": "1byj7lpw0ahk6k63sbc9859v68f28hpaab41dxsjj1ggjdfv9i8g",
"registry+https://github.com/rust-lang/crates.io-index#async-io@2.3.1": "0rggn074kbqxxajci1aq14b17gp75rw9l6rpbazcv9q0bc6ap5wg",
"registry+https://github.com/rust-lang/crates.io-index#async-lock@2.8.0": "0asq5xdzgp3d5m82y5rg7a0k9q0g95jy6mgc7ivl334x7qlp4wi8",
"registry+https://github.com/rust-lang/crates.io-index#async-lock@3.3.0": "0yxflkfw46rad4lv86f59b5z555dlfmg1riz1n8830rgi0qb8d6h",
"registry+https://github.com/rust-lang/crates.io-index#async-std@1.12.0": "0pbgxhyb97h4n0451r26njvr20ywqsbm6y1wjllnp4if82s5nmk2",
"registry+https://github.com/rust-lang/crates.io-index#async-task@4.7.0": "16975vx6aqy5yf16fs9xz5vx1zq8mwkzfmykvcilc1j7b6c6xczv",
"registry+https://github.com/rust-lang/crates.io-index#async-trait@0.1.77": "1adf1jh2yg39rkpmqjqyr9xyd6849p0d95425i6imgbhx0syx069",
"registry+https://github.com/rust-lang/crates.io-index#atoi@2.0.0": "0a05h42fggmy7h0ajjv6m7z72l924i7igbx13hk9d8pyign9k3gj",
"registry+https://github.com/rust-lang/crates.io-index#atomic-waker@1.1.2": "1h5av1lw56m0jf0fd3bchxq8a30xv0b4wv8s4zkp4s0i7mfvs18m",
"registry+https://github.com/rust-lang/crates.io-index#atomic-write-file@0.1.2": "0dl4x0srdwjxm3zz3fj1c7m44i3b7mjiad550fqklj1n4bfbxkgd",
"registry+https://github.com/rust-lang/crates.io-index#autocfg@0.1.8": "0y4vw4l4izdxq1v0rrhvmlbqvalrqrmk60v1z0dqlgnlbzkl7phd",
"registry+https://github.com/rust-lang/crates.io-index#autocfg@1.1.0": "1ylp3cb47ylzabimazvbz9ms6ap784zhb6syaz6c1jqpmcmq0s6l",
"registry+https://github.com/rust-lang/crates.io-index#backtrace@0.3.69": "0dsq23dhw4pfndkx2nsa1ml2g31idm7ss7ljxp8d57avygivg290",
"registry+https://github.com/rust-lang/crates.io-index#base64@0.21.5": "1y8x2xs9nszj5ix7gg4ycn5a6wy7ca74zxwqri3bdqzdjha6lqrm",
"registry+https://github.com/rust-lang/crates.io-index#base64@0.9.3": "0hs62r35bgxslawyrn1vp9rmvrkkm76fqv0vqcwd048vs876r7a8",
"registry+https://github.com/rust-lang/crates.io-index#base64ct@1.6.0": "0nvdba4jb8aikv60az40x2w1y96sjdq8z3yp09rwzmkhiwv1lg4c",
"registry+https://github.com/rust-lang/crates.io-index#bit-set@0.5.3": "1wcm9vxi00ma4rcxkl3pzzjli6ihrpn9cfdi0c5b4cvga2mxs007",
"registry+https://github.com/rust-lang/crates.io-index#bit-vec@0.6.3": "1ywqjnv60cdh1slhz67psnp422md6jdliji6alq0gmly2xm9p7rl",
"registry+https://github.com/rust-lang/crates.io-index#bit_field@0.10.2": "0qav5rpm4hqc33vmf4vc4r0mh51yjx5vmd9zhih26n9yjs3730nw",
"registry+https://github.com/rust-lang/crates.io-index#bitflags@1.3.2": "12ki6w8gn1ldq7yz9y680llwk5gmrhrzszaa17g1sbrw2r2qvwxy",
"registry+https://github.com/rust-lang/crates.io-index#bitflags@2.4.1": "01ryy3kd671b0ll4bhdvhsz67vwz1lz53fz504injrd7wpv64xrj",
"registry+https://github.com/rust-lang/crates.io-index#block-buffer@0.10.4": "0w9sa2ypmrsqqvc20nhwr75wbb5cjr4kkyhpjm1z1lv2kdicfy1h",
"registry+https://github.com/rust-lang/crates.io-index#blocking@1.5.1": "064i3d6b8ln34fgdw49nmx9m36bwi3r3nv8c9xhcrpf4ilz92dva",
"registry+https://github.com/rust-lang/crates.io-index#build_html@2.4.0": "188nibbsv33vgjjiq9cn2irsgdb75gxfipavcavnyydcwxpzw21i",
"registry+https://github.com/rust-lang/crates.io-index#bumpalo@3.14.0": "1v4arnv9kwk54v5d0qqpv4vyw2sgr660nk0w3apzixi1cm3yfc3z",
"registry+https://github.com/rust-lang/crates.io-index#bytemuck@1.14.0": "1ik1ma5n3bg700skkzhx50zjk7kj7mbsphi773if17l04pn2hk9p",
"registry+https://github.com/rust-lang/crates.io-index#byteorder@1.5.0": "0jzncxyf404mwqdbspihyzpkndfgda450l0893pz5xj685cg5l0z",
"registry+https://github.com/rust-lang/crates.io-index#bytes@1.5.0": "08w2i8ac912l8vlvkv3q51cd4gr09pwlg3sjsjffcizlrb0i5gd2",
"registry+https://github.com/rust-lang/crates.io-index#cairo-rs@0.18.3": "18d80lk853bjhx36rjaj78clzfjrmlgi01863drnmshdgxi16dpk",
"registry+https://github.com/rust-lang/crates.io-index#cairo-sys-rs@0.18.2": "0lfsxl7ylw3phbnwmz3k58j1gnqi6kc2hdc7g3bb7f4hwnl9yp38",
"registry+https://github.com/rust-lang/crates.io-index#cc@1.0.83": "1l643zidlb5iy1dskc5ggqs4wqa29a02f44piczqc8zcnsq4y5zi",
"registry+https://github.com/rust-lang/crates.io-index#cfg-expr@0.15.5": "1cqicd9qi8mzzgh63dw03zhbdihqfl3lbiklrkynyzkq67s5m483",
"registry+https://github.com/rust-lang/crates.io-index#cfg-if@1.0.0": "1za0vb97n4brpzpv8lsbnzmq5r8f2b0cpqqr0sy8h5bn751xxwds",
"registry+https://github.com/rust-lang/crates.io-index#chrono-tz-build@0.2.1": "03rmzd69cn7fp0fgkjr5042b3g54s2l941afjm3001ls7kqkjgj3",
"registry+https://github.com/rust-lang/crates.io-index#chrono-tz@0.8.4": "0xhd3dsfs72im0sbc7w889lfy7bxgjlbvqhj5a1yvxhxwb08acg2",
"registry+https://github.com/rust-lang/crates.io-index#chrono@0.4.31": "0f6vg67pipm8cziad2yms6a639pssnvysk1m05dd9crymmdnhb3z",
"registry+https://github.com/rust-lang/crates.io-index#clap@4.4.11": "1wj5gb2fnqls00zfahg3490bdfc36d9cwpl80qjacb5jyrqzdbxz",
"registry+https://github.com/rust-lang/crates.io-index#clap_builder@4.4.11": "1fxdsmw1ilgswz3lg2hjlvsdyyz04k78scjirlbd7c9bc83ba5m2",
"registry+https://github.com/rust-lang/crates.io-index#clap_derive@4.4.7": "0hk4hcxl56qwqsf4hmf7c0gr19r9fbxk0ah2bgkr36pmmaph966g",
"registry+https://github.com/rust-lang/crates.io-index#clap_lex@0.6.0": "1l8bragdvim7mva9flvd159dskn2bdkpl0jqrr41wnjfn8pcfbvh",
"registry+https://github.com/rust-lang/crates.io-index#cloudabi@0.0.3": "0kxcg83jlihy0phnd2g8c2c303px3l2p3pkjz357ll6llnd5pz6x",
"registry+https://github.com/rust-lang/crates.io-index#color_quant@1.1.0": "12q1n427h2bbmmm1mnglr57jaz2dj9apk0plcxw7nwqiai7qjyrx",
"registry+https://github.com/rust-lang/crates.io-index#colorchoice@1.0.0": "1ix7w85kwvyybwi2jdkl3yva2r2bvdcc3ka2grjfzfgrapqimgxc",
"registry+https://github.com/rust-lang/crates.io-index#concurrent-queue@2.4.0": "0qvk23ynj311adb4z7v89wk3bs65blps4n24q8rgl23vjk6lhq6i",
"registry+https://github.com/rust-lang/crates.io-index#const-oid@0.9.6": "1y0jnqaq7p2wvspnx7qj76m7hjcqpz73qzvr9l2p9n2s51vr6if2",
"registry+https://github.com/rust-lang/crates.io-index#cookie@0.17.0": "096c52jg9iq4lfcps2psncswv33fc30mmnaa2sbzzcfcw71kgyvy",
"registry+https://github.com/rust-lang/crates.io-index#cool_asserts@2.0.3": "1v18dg7ifx41k2f82j3gsnpm1fg9wk5s4zv7sf42c7pnad72b7zf",
"registry+https://github.com/rust-lang/crates.io-index#core-foundation-sys@0.8.6": "13w6sdf06r0hn7bx2b45zxsg1mm2phz34jikm6xc5qrbr6djpsh6",
"registry+https://github.com/rust-lang/crates.io-index#core-foundation@0.9.4": "13zvbbj07yk3b61b8fhwfzhy35535a583irf23vlcg59j7h9bqci",
"registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.11": "1l0gzsyy576n017g9bf0vkv5hhg9cpz1h1libxyfdlzcgbh0yhnf",
"registry+https://github.com/rust-lang/crates.io-index#crc-catalog@2.4.0": "1xg7sz82w3nxp1jfn425fvn1clvbzb3zgblmxsyqpys0dckp9lqr",
"registry+https://github.com/rust-lang/crates.io-index#crc32fast@1.3.2": "03c8f29yx293yf43xar946xbls1g60c207m9drf8ilqhr25vsh5m",
"registry+https://github.com/rust-lang/crates.io-index#crc@3.0.1": "1zkx87a5x06xfd6xm5956w4vmdfs0wcxpsn7iwj5jbp2rcapmv46",
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-deque@0.8.4": "0la7fx9n1vbx3h23va0xmcy36hziql1pkik08s3j3asv4479ma7w",
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-epoch@0.9.16": "1anr32r8px0vb65cgwbwp3zhqz69scz5dgq9bmx54w5qa59yjbrd",
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-queue@0.3.9": "0lz17pgydh29w8brld8dysi1m4n5bxfpnj8w9bxk0q6xpyyzbg5r",
"registry+https://github.com/rust-lang/crates.io-index#crossbeam-utils@0.8.17": "13y7wh993i7q71kg6wcfj65w3rlmizzrz7cqgz1l9whlgw9rcvf0",
"registry+https://github.com/rust-lang/crates.io-index#crunchy@0.2.2": "1dx9mypwd5mpfbbajm78xcrg5lirqk7934ik980mmaffg3hdm0bs",
"registry+https://github.com/rust-lang/crates.io-index#crypto-common@0.1.6": "1cvby95a6xg7kxdz5ln3rl9xh66nz66w46mm3g56ri1z5x815yqv",
"registry+https://github.com/rust-lang/crates.io-index#data-encoding@2.5.0": "1rcbnwfmfxhlshzbn3r7srm3azqha3mn33yxyqxkzz2wpqcjm5ky",
"registry+https://github.com/rust-lang/crates.io-index#deflate@0.8.6": "0x6iqlayg129w63999kz97m279m0jj4x4sm6gkqlvmp73y70yxvk",
"registry+https://github.com/rust-lang/crates.io-index#der@0.7.8": "070bwiyr80800h31c5zd96ckkgagfjgnrrdmz3dzg2lccsd3dypz",
"registry+https://github.com/rust-lang/crates.io-index#deranged@0.3.10": "1p4i64nkadamksa943d6gk39sl1kximz0xr69n408fvsl1q0vcwf",
"registry+https://github.com/rust-lang/crates.io-index#digest@0.10.7": "14p2n6ih29x81akj097lvz7wi9b6b9hvls0lwrv7b6xwyy0s5ncy",
"registry+https://github.com/rust-lang/crates.io-index#dimensioned@0.7.0": "09ky8s3higkf677lmyqg30hmj66gpg7hx907s6hfvbk2a9av05r5",
"registry+https://github.com/rust-lang/crates.io-index#dimensioned@0.8.0": "15s3j4ry943xqlac63bp81sgdk9s3yilysabzww35j9ibmnaic50",
"registry+https://github.com/rust-lang/crates.io-index#displaydoc@0.2.4": "0p8pyg10csc782qlwx3znr6qx46ni96m1qh597kmyrf6s3s8axa8",
"registry+https://github.com/rust-lang/crates.io-index#dotenvy@0.15.7": "16s3n973n5aqym02692i1npb079n5mb0fwql42ikmwn8wnrrbbqs",
"registry+https://github.com/rust-lang/crates.io-index#either@1.9.0": "01qy3anr7jal5lpc20791vxrw0nl6vksb5j7x56q2fycgcyy8sm2",
"registry+https://github.com/rust-lang/crates.io-index#encoding_rs@0.8.33": "1qa5k4a0ipdrxq4xg9amms9r9pnnfn7nfh2i9m3mw0ka563b6s3j",
"registry+https://github.com/rust-lang/crates.io-index#env_logger@0.10.1": "1kmy9xmfjaqfvd4wkxr1f7d16ld3h9b487vqs2q9r0s8f3kg7cwm",
"registry+https://github.com/rust-lang/crates.io-index#equivalent@1.0.1": "1malmx5f4lkfvqasz319lq6gb3ddg19yzf9s8cykfsgzdmyq0hsl",
"registry+https://github.com/rust-lang/crates.io-index#errno@0.3.8": "0ia28ylfsp36i27g1qih875cyyy4by2grf80ki8vhgh6vinf8n52",
"registry+https://github.com/rust-lang/crates.io-index#etcetera@0.8.0": "0hxrsn75dirbjhwgkdkh0pnpqrnq17ypyhjpjaypgax1hd91nv8k",
"registry+https://github.com/rust-lang/crates.io-index#event-listener-strategy@0.4.0": "1lwprdjqp2ibbxhgm9khw7s7y7k4xiqj5i5yprqiks6mnrq4v3lm",
"registry+https://github.com/rust-lang/crates.io-index#event-listener@2.5.3": "1q4w3pndc518crld6zsqvvpy9lkzwahp2zgza9kbzmmqh9gif1h2",
"registry+https://github.com/rust-lang/crates.io-index#event-listener@4.0.1": "04k7qbi5kgs36s905gxijj41kcr78xs2s6cp6vbg50254z7wvwl4",
"registry+https://github.com/rust-lang/crates.io-index#exr@1.71.0": "1a58k179b0h8zpf1cfgc2vl60j2syg7cdgdzp9j6cgmb6lgpcal3",
"registry+https://github.com/rust-lang/crates.io-index#fastrand@1.9.0": "1gh12m56265ihdbzh46bhh0jf74i197wm51jg1cw75q7ggi96475",
"registry+https://github.com/rust-lang/crates.io-index#fastrand@2.0.1": "19flpv5zbzpf0rk4x77z4zf25in0brg8l7m304d3yrf47qvwxjr5",
"registry+https://github.com/rust-lang/crates.io-index#fdeflate@0.3.1": "0s5885wdsih2hqx3hsl7l8cl3666fgsgiwvglifzy229hpydmmk4",
"registry+https://github.com/rust-lang/crates.io-index#field-offset@0.3.6": "0zq5sssaa2ckmcmxxbly8qgz3sxpb8g1lwv90sdh1z74qif2gqiq",
"registry+https://github.com/rust-lang/crates.io-index#finl_unicode@1.2.0": "1ipdx778849czik798sjbgk5yhwxqybydac18d2g9jb20dxdrkwg",
"registry+https://github.com/rust-lang/crates.io-index#flate2@1.0.28": "03llhsh4gqdirnfxxb9g2w9n0721dyn4yjir3pz7z4vjaxb3yc26",
"registry+https://github.com/rust-lang/crates.io-index#fluent-bundle@0.15.2": "1zbzm13rfz7fay7bps7jd4j1pdnlxmdzzfymyq2iawf9vq0wchp2",
"registry+https://github.com/rust-lang/crates.io-index#fluent-langneg@0.13.0": "152yxplc11vmxkslvmaqak9x86xnavnhdqyhrh38ym37jscd0jic",
"registry+https://github.com/rust-lang/crates.io-index#fluent-syntax@0.11.0": "0y6ac7z7sbv51nsa6km5z8rkjj4nvqk91vlghq1ck5c3cjbyvay0",
"registry+https://github.com/rust-lang/crates.io-index#fluent@0.16.0": "19s7z0gw95qdsp9hhc00xcy11nwhnx93kknjmdvdnna435w97xk1",
"registry+https://github.com/rust-lang/crates.io-index#flume@0.11.0": "10girdbqn77wi802pdh55lwbmymy437k7kklnvj12aaiwaflbb2m",
"registry+https://github.com/rust-lang/crates.io-index#fnv@1.0.7": "1hc2mcqha06aibcaza94vbi81j6pr9a1bbxrxjfhc91zin8yr7iz",
"registry+https://github.com/rust-lang/crates.io-index#foreign-types-shared@0.1.1": "0jxgzd04ra4imjv8jgkmdq59kj8fsz6w4zxsbmlai34h26225c00",
"registry+https://github.com/rust-lang/crates.io-index#foreign-types@0.3.2": "1cgk0vyd7r45cj769jym4a6s7vwshvd0z4bqrb92q1fwibmkkwzn",
"registry+https://github.com/rust-lang/crates.io-index#form_urlencoded@1.2.1": "0milh8x7nl4f450s3ddhg57a3flcv6yq8hlkyk6fyr3mcb128dp1",
"registry+https://github.com/rust-lang/crates.io-index#fuchsia-cprng@0.1.1": "1fnkqrbz7ixxzsb04bsz9p0zzazanma8znfdqjvh39n14vapfvx0",
"registry+https://github.com/rust-lang/crates.io-index#futures-channel@0.3.29": "1jxsifvrbqzdadk0svbax71cba5d3qg3wgjq8i160mxmd1kdckgz",
"registry+https://github.com/rust-lang/crates.io-index#futures-core@0.3.29": "1308bpj0g36nhx2y6bl4mm6f1gnh9xyvvw2q2wpdgnb6dv3247gb",
"registry+https://github.com/rust-lang/crates.io-index#futures-executor@0.3.29": "1g4pjni0sw28djx6mlcfz584abm2lpifz86cmng0kkxh7mlvhkqg",
"registry+https://github.com/rust-lang/crates.io-index#futures-intrusive@0.5.0": "0vwm08d1pli6bdaj0i7xhk3476qlx4pll6i0w03gzdnh7lh0r4qx",
"registry+https://github.com/rust-lang/crates.io-index#futures-io@0.3.29": "1ajsljgny3zfxwahba9byjzclrgvm1ypakca8z854k2w7cb4mwwb",
"registry+https://github.com/rust-lang/crates.io-index#futures-lite@1.13.0": "1kkbqhaib68nzmys2dc8j9fl2bwzf2s91jfk13lb2q3nwhfdbaa9",
"registry+https://github.com/rust-lang/crates.io-index#futures-lite@2.2.0": "1flj85i6xm0rjicxixmajrp6rhq8i4bnbzffmrd6h23ln8jshns4",
"registry+https://github.com/rust-lang/crates.io-index#futures-macro@0.3.29": "1nwd18i8kvpkdfwm045hddjli0n96zi7pn6f99zi9c74j7ym7cak",
"registry+https://github.com/rust-lang/crates.io-index#futures-sink@0.3.29": "05q8jykqddxzp8nwf00wjk5m5mqi546d7i8hsxma7hiqxrw36vg3",
"registry+https://github.com/rust-lang/crates.io-index#futures-task@0.3.29": "1qmsss8rb5ppql4qvd4r70h9gpfcpd0bg2b3qilxrnhdkc397lgg",
"registry+https://github.com/rust-lang/crates.io-index#futures-util@0.3.29": "0141rkqh0psj4h8x8lgsl1p29dhqr7z2wcixkcbs60z74kb2d5d1",
"registry+https://github.com/rust-lang/crates.io-index#futures@0.3.29": "0dak2ilpcmyjrb1j54fzy9hlw6vd10vqljq9gd59pbrq9dqr00ns",
"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf-sys@0.18.0": "1xya543c4ffd2n7aiwwrdxsyc9casdbasafi6ixcknafckm3k61z",
"registry+https://github.com/rust-lang/crates.io-index#gdk-pixbuf@0.18.3": "0b68ssdyapvq3bgsna9frabbzhjkvvzz8jld4mxkphr29nvk4vs4",
"registry+https://github.com/rust-lang/crates.io-index#gdk4-sys@0.7.2": "1w7yvir565sjrrw828lss07749hfpfsr19jdjzwivkx36brl7ayv",
"registry+https://github.com/rust-lang/crates.io-index#gdk4@0.7.3": "1xiacc63p73apr033gjrb9dsk0y4yxnsljwfxbwfry41snd03nvy",
"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.11.2": "0a7w8w0rg47nmcinnfzv443lcyb8mplwc251p1jyr5xj2yh6wzv6",
"registry+https://github.com/rust-lang/crates.io-index#generic-array@0.14.7": "16lyyrzrljfq424c3n8kfwkqihlimmsg5nhshbbp48np3yjrqr45",
"registry+https://github.com/rust-lang/crates.io-index#getrandom@0.2.11": "03q7120cc2kn7ry013i67zmjl2g9q73h1ks5z08hq5v9syz0d47y",
"registry+https://github.com/rust-lang/crates.io-index#gif@0.11.4": "01hbw3isapzpzff8l6aw55jnaqx2bcscrbwyf3rglkbbfp397p9y",
"registry+https://github.com/rust-lang/crates.io-index#gif@0.12.0": "0ibhjyrslfv9qm400gp4hd50v9ibva01j4ab9bwiq1aycy9jayc0",
"registry+https://github.com/rust-lang/crates.io-index#gimli@0.28.1": "0lv23wc8rxvmjia3mcxc6hj9vkqnv1bqq0h8nzjcgf71mrxx6wa2",
"registry+https://github.com/rust-lang/crates.io-index#gio-sys@0.18.1": "1lip8z35iy9d184x2qwjxlbxi64q9cpayy7v1p5y9xdsa3w6smip",
"registry+https://github.com/rust-lang/crates.io-index#gio@0.18.4": "0wsc6mnx057s4ailacg99dwgna38dbqli5x7a6y9rdw75x9qzz6l",
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.16.3": "1z73bl10zmxwrv16v4f5wcky1f3z5a2v0hknca54al4k2p5ka695",
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.17.10": "05p7ab2vn8962cbchi7a6hndhvw64nqk4w5kpg5z53iizsgdfrbs",
"registry+https://github.com/rust-lang/crates.io-index#glib-build-tools@0.18.0": "0p5c2ayiam5bkp9wvq9f9ihwp06nqs5j801npjlwnhrl8rpwac9l",
"registry+https://github.com/rust-lang/crates.io-index#glib-macros@0.18.3": "19crnw5a57w02njpbsmdqwbkncl6hw6g3mv554y8dqzcrri3jybj",
"registry+https://github.com/rust-lang/crates.io-index#glib-sys@0.18.1": "164qhsfmlzd5mhyxs8123jzbdfldwxbikfpq5cysj3lddbmy4g06",
"registry+https://github.com/rust-lang/crates.io-index#glib@0.18.4": "0kjws6ns6dym48nzxz9skhipk55flc2hy5q5kzg4w12wvizvs6wm",
"registry+https://github.com/rust-lang/crates.io-index#gloo-timers@0.2.6": "0p2yqcxw0q9kclhwpgshq1r4ijns07nmmagll3lvrgl7pdk5m6cv",
"registry+https://github.com/rust-lang/crates.io-index#gobject-sys@0.18.0": "0i6fhp3m6vs3wkzyc22rk2cqj68qvgddxmpaai34l72da5xi4l08",
"registry+https://github.com/rust-lang/crates.io-index#graphene-rs@0.18.1": "00f4q1ra4haap5i7lazwhkdgnb49fs8adk2nm6ki6mjhl76jh8iv",
"registry+https://github.com/rust-lang/crates.io-index#graphene-sys@0.18.1": "0n8zlg7z26lwpnvlqp1hjlgrs671skqwagdpm7r8i1zwx3748hfc",
"registry+https://github.com/rust-lang/crates.io-index#grid@0.9.0": "0iswdcxggyxp9m1rz0m7bfg4xacinvn78zp2fgfp0l0079x10d06",
"registry+https://github.com/rust-lang/crates.io-index#gsk4-sys@0.7.3": "0mbdlm9qi1hql48rr29vsj9vlqwc7gxg67wg1q19z67azwz9xg8j",
"registry+https://github.com/rust-lang/crates.io-index#gsk4@0.7.3": "0zhzs2dkgiinhgc11akpn2harq3x5n1iq21dnc4h689g3lsqx58d",
"registry+https://github.com/rust-lang/crates.io-index#gtk4-macros@0.7.2": "0bw3cchiycf7dw1bw4p8946gv38azxy05a5w0ndgcmxnz6fc8znm",
"registry+https://github.com/rust-lang/crates.io-index#gtk4-sys@0.7.3": "1f2ylskyqkjdik9fij2m46pra4jagnif5xyalbxfk3334fmc9n2l",
"registry+https://github.com/rust-lang/crates.io-index#gtk4@0.7.3": "0hh8nzglmz94v1m1h6vy8z12m6fr7ia467ry0md5fa4p7sm53sss",
"registry+https://github.com/rust-lang/crates.io-index#h2@0.3.22": "0y41jlflvw8niifdirgng67zdmic62cjf5m2z69hzrpn5qr50qjd",
"registry+https://github.com/rust-lang/crates.io-index#half@2.2.1": "1l1gdlzxgm7wc8xl5fxas20kfi1j35iyb7vfjkghbdzijcvazd02",
"registry+https://github.com/rust-lang/crates.io-index#hashbrown@0.14.3": "012nywlg0lj9kwanh69my5x67vjlfmzfi9a0rq4qvis2j8fil3r9",
"registry+https://github.com/rust-lang/crates.io-index#hashlink@0.8.4": "1xy8agkyp0llbqk9fcffc1xblayrrywlyrm2a7v93x8zygm4y2g8",
"registry+https://github.com/rust-lang/crates.io-index#headers-core@0.2.0": "0ab469xfpd411mc3dhmjhmzrhqikzyj8a17jn5bkj9zfpy0n9xp7",
"registry+https://github.com/rust-lang/crates.io-index#headers@0.3.9": "0w62gnwh2p1lml0zqdkrx9dp438881nhz32zrzdy61qa0a9kns06",
"registry+https://github.com/rust-lang/crates.io-index#heck@0.4.1": "1a7mqsnycv5z4z5vnv1k34548jzmc0ajic7c1j8jsaspnhw5ql4m",
"registry+https://github.com/rust-lang/crates.io-index#hermit-abi@0.3.3": "1dyc8qsjh876n74a3rcz8h43s27nj1sypdhsn2ms61bd3b47wzyp",
"registry+https://github.com/rust-lang/crates.io-index#hex-string@0.1.0": "02sgrgrbp693jv0v5iga7z47y6aj93cq0ia39finby9x17fw53l4",
"registry+https://github.com/rust-lang/crates.io-index#hex@0.4.3": "0w1a4davm1lgzpamwnba907aysmlrnygbqmfis2mqjx5m552a93z",
"registry+https://github.com/rust-lang/crates.io-index#hkdf@0.12.4": "1xxxzcarz151p1b858yn5skmhyrvn8fs4ivx5km3i1kjmnr8wpvv",
"registry+https://github.com/rust-lang/crates.io-index#hmac@0.12.1": "0pmbr069sfg76z7wsssfk5ddcqd9ncp79fyz6zcm6yn115yc6jbc",
"registry+https://github.com/rust-lang/crates.io-index#home@0.5.9": "19grxyg35rqfd802pcc9ys1q3lafzlcjcv2pl2s5q8xpyr5kblg3",
"registry+https://github.com/rust-lang/crates.io-index#http-body@0.4.6": "1lmyjfk6bqk6k9gkn1dxq770sb78pqbqshga241hr5p995bb5skw",
"registry+https://github.com/rust-lang/crates.io-index#http@0.2.11": "1fwz3mhh86h5kfnr5767jlx9agpdggclq7xsqx930fflzakb2iw9",
"registry+https://github.com/rust-lang/crates.io-index#http@1.0.0": "1sllw565jn8r5w7h928nsfqq33x586pyasdfr7vid01scwwgsamk",
"registry+https://github.com/rust-lang/crates.io-index#httparse@1.8.0": "010rrfahm1jss3p022fqf3j3jmm72vhn4iqhykahb9ynpaag75yq",
"registry+https://github.com/rust-lang/crates.io-index#httpdate@1.0.3": "1aa9rd2sac0zhjqh24c9xvir96g188zldkx0hr6dnnlx5904cfyz",
"registry+https://github.com/rust-lang/crates.io-index#humantime@2.1.0": "1r55pfkkf5v0ji1x6izrjwdq9v6sc7bv99xj6srywcar37xmnfls",
"registry+https://github.com/rust-lang/crates.io-index#hyper-tls@0.5.0": "01crgy13102iagakf6q4mb75dprzr7ps1gj0l5hxm1cvm7gks66n",
"registry+https://github.com/rust-lang/crates.io-index#hyper@0.10.16": "0wwjh9p3mzvg3fss2lqz5r7ddcgl1fh9w6my2j69d6k0lbcm41ha",
"registry+https://github.com/rust-lang/crates.io-index#hyper@0.14.28": "107gkvqx4h9bl17d602zkm2dgpfq86l2dr36yzfsi8l3xcsy35mz",
"registry+https://github.com/rust-lang/crates.io-index#iana-time-zone-haiku@0.1.2": "17r6jmj31chn7xs9698r122mapq85mfnv98bb4pg6spm0si2f67k",
"registry+https://github.com/rust-lang/crates.io-index#iana-time-zone@0.1.58": "081vcr8z8ddhl5r1ywif6grnswk01b2ac4nks2bhn8zzdimvh9l3",
"registry+https://github.com/rust-lang/crates.io-index#idna@0.1.5": "0kl4gs5kaydn4v07c6ka33spm9qdh2np0x7iw7g5zd8z1c7rxw1q",
"registry+https://github.com/rust-lang/crates.io-index#idna@0.5.0": "1xhjrcjqq0l5bpzvdgylvpkgk94panxgsirzhjnnqfdgc4a9nkb3",
"registry+https://github.com/rust-lang/crates.io-index#image@0.23.14": "18gn2f7xp30pf9aqka877knlq308khxqiwjvsccvzaa4f9zcpzr4",
"registry+https://github.com/rust-lang/crates.io-index#image@0.24.7": "04d7f25b8nlszfv9a474n4a0al4m2sv9gqj3yiphhqr0syyzsgbg",
"registry+https://github.com/rust-lang/crates.io-index#indent_write@2.2.0": "1hqjp80argdskrhd66g9sh542yxy8qi77j6rc69qd0l7l52rdzhc",
"registry+https://github.com/rust-lang/crates.io-index#indexmap@2.1.0": "07rxrqmryr1xfnmhrjlz8ic6jw28v6h5cig3ws2c9d0wifhy2c6m",
"registry+https://github.com/rust-lang/crates.io-index#instant@0.1.12": "0b2bx5qdlwayriidhrag8vhy10kdfimfhmb3jnjmsz2h9j1bwnvs",
"registry+https://github.com/rust-lang/crates.io-index#intl-memoizer@0.5.1": "0vx6cji8ifw77zrgipwmvy1i3v43dcm58hwjxpb1h29i98z46463",
"registry+https://github.com/rust-lang/crates.io-index#intl_pluralrules@7.0.2": "0wprd3h6h8nfj62d8xk71h178q7zfn3srxm787w4sawsqavsg3h7",
"registry+https://github.com/rust-lang/crates.io-index#io-lifetimes@1.0.11": "1hph5lz4wd3drnn6saakwxr497liznpfnv70via6s0v8x6pbkrza",
"registry+https://github.com/rust-lang/crates.io-index#ipnet@2.9.0": "1hzrcysgwf0knf83ahb3535hrkw63mil88iqc6kjaryfblrqylcg",
"registry+https://github.com/rust-lang/crates.io-index#iron@0.6.1": "1s4mf8395f693nhwsr0znw3j5frzn56gzllypyl50il85p50ily6",
"registry+https://github.com/rust-lang/crates.io-index#is-terminal@0.4.9": "12xgvc7nsrp3pn8hcxajfhbli2l5wnh3679y2fmky88nhj4qj26b",
"registry+https://github.com/rust-lang/crates.io-index#itertools@0.12.0": "1c07gzdlc6a1c8p8jrvvw3gs52bss3y58cs2s21d9i978l36pnr5",
"registry+https://github.com/rust-lang/crates.io-index#itoa@1.0.10": "0k7xjfki7mnv6yzjrbnbnjllg86acmbnk4izz2jmm1hx2wd6v95i",
"registry+https://github.com/rust-lang/crates.io-index#jpeg-decoder@0.1.22": "1wnh0bmmswpgwhgmlizz545x8334nlbmkq8imy9k224ri3am7792",
"registry+https://github.com/rust-lang/crates.io-index#jpeg-decoder@0.3.0": "0gkv0zx95i4fr40fj1a10d70lqi6lfyia8r5q8qjxj8j4pj0005w",
"registry+https://github.com/rust-lang/crates.io-index#js-sys@0.3.66": "1ji9la5ydg0vy17q54i7dnwc0wwb9zkx662w1583pblylm6wdsff",
"registry+https://github.com/rust-lang/crates.io-index#kv-log-macro@1.0.7": "0zwp4bxkkp87rl7xy2dain77z977rvcry1gmr5bssdbn541v7s0d",
"registry+https://github.com/rust-lang/crates.io-index#language-tags@0.2.2": "16hrjdpa827carq5x4b8zhas24d8kg4s16m6nmmn1kb7cr5qh7d9",
"registry+https://github.com/rust-lang/crates.io-index#lazy_static@1.4.0": "0in6ikhw8mgl33wjv6q6xfrb5b9jr16q8ygjy803fay4zcisvaz2",
"registry+https://github.com/rust-lang/crates.io-index#lebe@0.5.2": "1j2l6chx19qpa5gqcw434j83gyskq3g2cnffrbl3842ymlmpq203",
"registry+https://github.com/rust-lang/crates.io-index#libadwaita-sys@0.5.3": "16n6xsy6jhbj0jbpz8yvql6c9b89a99v9vhdz5s37mg1inisl42y",
"registry+https://github.com/rust-lang/crates.io-index#libadwaita@0.5.3": "174pzn9dwsk8ikvrhx13vkh0zrpvb3rhg9yd2q5d2zjh0q6fgrrg",
"registry+https://github.com/rust-lang/crates.io-index#libc@0.2.151": "1x28f0zgp4zcwr891p8n9ag9w371sbib30vp4y6hi2052frplb9h",
"registry+https://github.com/rust-lang/crates.io-index#libm@0.2.8": "0n4hk1rs8pzw8hdfmwn96c4568s93kfxqgcqswr7sajd2diaihjf",
"registry+https://github.com/rust-lang/crates.io-index#libsqlite3-sys@0.27.0": "05pp60ncrmyjlxxjj187808jkvpxm06w5lvvdwwvxd2qrmnj4kng",
"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.3.8": "068mbigb3frrxvbi5g61lx25kksy98f2qgkvc4xg8zxznwp98lzg",
"registry+https://github.com/rust-lang/crates.io-index#linux-raw-sys@0.4.12": "0mhlla3gk1jgn6mrq9s255rvvq8a1w3yk2vpjiwsd6hmmy1imkf4",
"registry+https://github.com/rust-lang/crates.io-index#lock_api@0.4.11": "0iggx0h4jx63xm35861106af3jkxq06fpqhpkhgw0axi2n38y5iw",
"registry+https://github.com/rust-lang/crates.io-index#log@0.3.9": "0jq23hhn5h35k7pa8r7wqnsywji6x3wn1q5q7lif5q536if8v7p1",
"registry+https://github.com/rust-lang/crates.io-index#log@0.4.20": "13rf7wphnwd61vazpxr7fiycin6cb1g8fmvgqg18i464p0y1drmm",
"registry+https://github.com/rust-lang/crates.io-index#logger@0.4.0": "14xlxvkspcfnspjil0xi63qj5cybxn1hjmr5gq8m4v1g9k5p54bc",
"registry+https://github.com/rust-lang/crates.io-index#matches@0.1.10": "1994402fq4viys7pjhzisj4wcw894l53g798kkm2y74laxk0jci5",
"registry+https://github.com/rust-lang/crates.io-index#md-5@0.10.6": "1kvq5rnpm4fzwmyv5nmnxygdhhb2369888a06gdc9pxyrzh7x7nq",
"registry+https://github.com/rust-lang/crates.io-index#memchr@2.6.4": "0rq1ka8790ns41j147npvxcqcl2anxyngsdimy85ag2api0fwrgn",
"registry+https://github.com/rust-lang/crates.io-index#memoffset@0.9.0": "0v20ihhdzkfw1jx00a7zjpk2dcp5qjq6lz302nyqamd9c4f4nqss",
"registry+https://github.com/rust-lang/crates.io-index#mime@0.2.6": "1q1s1ax1gaz8ld3513nvhidfwnik5asbs1ma3hp6inp5dn56nqms",
"registry+https://github.com/rust-lang/crates.io-index#mime@0.3.17": "16hkibgvb9klh0w0jk5crr5xv90l3wlf77ggymzjmvl1818vnxv8",
"registry+https://github.com/rust-lang/crates.io-index#mime_guess@1.8.8": "18qcd5aa3363mb742y7lf39j7ha88pkzbv9ff2qidlsdxsjjjs91",
"registry+https://github.com/rust-lang/crates.io-index#mime_guess@2.0.4": "1vs28rxnbfwil6f48hh58lfcx90klcvg68gxdc60spwa4cy2d4j1",
"registry+https://github.com/rust-lang/crates.io-index#minimal-lexical@0.2.1": "16ppc5g84aijpri4jzv14rvcnslvlpphbszc7zzp6vfkddf4qdb8",
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.3.7": "0dblrhgbm0wa8jjl8cjp81akaj36yna92df4z1h9b26n3spal7br",
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.4.4": "0jsfv00hl5rmx1nijn59sr9jmjd4rjnjhh4kdjy8d187iklih9d9",
"registry+https://github.com/rust-lang/crates.io-index#miniz_oxide@0.7.1": "1ivl3rbbdm53bzscrd01g60l46lz5krl270487d8lhjvwl5hx0g7",
"registry+https://github.com/rust-lang/crates.io-index#mio@0.8.10": "02gyaxvaia9zzi4drrw59k9s0j6pa5d1y2kv7iplwjipdqlhngcg",
"registry+https://github.com/rust-lang/crates.io-index#modifier@0.1.0": "0n3fmgli1nsskl0whrfzm1gk0rmwwl6pw1q4nb9sqqmn5h8wkxa1",
"registry+https://github.com/rust-lang/crates.io-index#multer@2.1.0": "1hjiphaypj3phqaj5igrzcia9xfmf4rr4ddigbh8zzb96k1bvb01",
"registry+https://github.com/rust-lang/crates.io-index#nary_tree@0.4.3": "1iqray1a716995l9mmvz5sfqrwg9a235bvrkpcn8bcqwjnwfv1pv",
"registry+https://github.com/rust-lang/crates.io-index#native-tls@0.2.11": "0bmrlg0fmzxaycjpkgkchi93av07v2yf9k33gc12ca9gqdrn28h7",
"registry+https://github.com/rust-lang/crates.io-index#nix@0.27.1": "0ly0kkmij5f0sqz35lx9czlbk6zpihb7yh1bsy4irzwfd2f4xc1f",
"registry+https://github.com/rust-lang/crates.io-index#no-std-compat@0.4.1": "132vrf710zsdp40yp1z3kgc2ss8pi0z4gmihsz3y7hl4dpd56f5r",
"registry+https://github.com/rust-lang/crates.io-index#nom@7.1.3": "0jha9901wxam390jcf5pfa0qqfrgh8li787jx2ip0yk5b8y9hwyj",
"registry+https://github.com/rust-lang/crates.io-index#num-bigint-dig@0.8.4": "0lb12df24wgxxbspz4gw1sf1kdqwvpdcpwq4fdlwg4gj41c1k16w",
"registry+https://github.com/rust-lang/crates.io-index#num-integer@0.1.45": "1ncwavvwdmsqzxnn65phv6c6nn72pnv9xhpmjd6a429mzf4k6p92",
"registry+https://github.com/rust-lang/crates.io-index#num-iter@0.1.43": "0lp22isvzmmnidbq9n5kbdh8gj0zm3yhxv1ddsn5rp65530fc0vx",
"registry+https://github.com/rust-lang/crates.io-index#num-rational@0.3.2": "01sgiwny9iflyxh2xz02sak71v2isc3x608hfdpwwzxi3j5l5b0j",
"registry+https://github.com/rust-lang/crates.io-index#num-rational@0.4.1": "1c0rb8x4avxy3jvvzv764yk7afipzxncfnqlb10r3h53s34s2f06",
"registry+https://github.com/rust-lang/crates.io-index#num-traits@0.2.17": "0z16bi5zwgfysz6765v3rd6whfbjpihx3mhsn4dg8dzj2c221qrr",
"registry+https://github.com/rust-lang/crates.io-index#num_cpus@1.16.0": "0hra6ihpnh06dvfvz9ipscys0xfqa9ca9hzp384d5m02ssvgqqa1",
"registry+https://github.com/rust-lang/crates.io-index#object@0.32.1": "1c02x4kvqpnl3wn7gz9idm4jrbirbycyqjgiw6lm1g9k77fzkxcw",
"registry+https://github.com/rust-lang/crates.io-index#once_cell@1.19.0": "14kvw7px5z96dk4dwdm1r9cqhhy2cyj1l5n5b29mynbb8yr15nrz",
"registry+https://github.com/rust-lang/crates.io-index#openssl-macros@0.1.1": "173xxvfc63rr5ybwqwylsir0vq6xsj4kxiv4hmg4c3vscdmncj59",
"registry+https://github.com/rust-lang/crates.io-index#openssl-probe@0.1.5": "1kq18qm48rvkwgcggfkqq6pm948190czqc94d6bm2sir5hq1l0gz",
"registry+https://github.com/rust-lang/crates.io-index#openssl-sys@0.9.97": "02s670ir38fsavphdna07144y41dkvrcfkwnjzg82zfrrlsavsn3",
"registry+https://github.com/rust-lang/crates.io-index#openssl@0.10.61": "0idv3n9n9f2sxq8cqzxvq44633vg5sx4n9q1p3g6dn66ikf1k13b",
"registry+https://github.com/rust-lang/crates.io-index#pango-sys@0.18.0": "1iaxalcaaj59cl9n10svh4g50v8jrc1a36kd7n9yahx8j7ikfrs3",
"registry+https://github.com/rust-lang/crates.io-index#pango@0.18.3": "1r5ygq7036sv7w32kp8yxr6vgggd54iaavh3yckanmq4xg0px8kw",
"registry+https://github.com/rust-lang/crates.io-index#parking@2.2.0": "1blwbkq6im1hfxp5wlbr475mw98rsyc0bbr2d5n16m38z253p0dv",
"registry+https://github.com/rust-lang/crates.io-index#parking_lot@0.12.1": "13r2xk7mnxfc5g0g6dkdxqdqad99j7s7z8zhzz4npw5r0g0v4hip",
"registry+https://github.com/rust-lang/crates.io-index#parking_lot_core@0.9.9": "13h0imw1aq86wj28gxkblhkzx6z1gk8q18n0v76qmmj6cliajhjc",
"registry+https://github.com/rust-lang/crates.io-index#parse-zoneinfo@0.3.0": "0h8g6jy4kckn2gk8sd5adaws180n1ip65xhzw5jxlq4w8ibg41f7",
"registry+https://github.com/rust-lang/crates.io-index#paste@1.0.14": "0k7d54zz8zrz0623l3xhvws61z5q2wd3hkwim6gylk8212placfy",
"registry+https://github.com/rust-lang/crates.io-index#pem-rfc7468@0.7.0": "04l4852scl4zdva31c1z6jafbak0ni5pi0j38ml108zwzjdrrcw8",
"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@1.0.1": "0cgq08v1fvr6bs5fvy390cz830lq4fak8havdasdacxcw790s09i",
"registry+https://github.com/rust-lang/crates.io-index#percent-encoding@2.3.1": "0gi8wgx0dcy8rnv1kywdv98lwcx67hz0a0zwpib5v2i08r88y573",
"registry+https://github.com/rust-lang/crates.io-index#phf@0.11.2": "1p03rsw66l7naqhpgr1a34r9yzi1gv9jh16g3fsk6wrwyfwdiqmd",
"registry+https://github.com/rust-lang/crates.io-index#phf@0.7.24": "066xwv4dr6056a9adlkarwp4n94kbpwngbmd47ngm3cfbyw49nmk",
"registry+https://github.com/rust-lang/crates.io-index#phf_codegen@0.11.2": "0nia6h4qfwaypvfch3pnq1nd2qj64dif4a6kai3b7rjrsf49dlz8",
"registry+https://github.com/rust-lang/crates.io-index#phf_codegen@0.7.24": "0zjiblicfm0nrmr2xxrs6pnf6zz2394wgch6dcbd8jijkq98agmh",
"registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.11.2": "1c14pjyxbcpwkdgw109f7581cc5fa3fnkzdq1ikvx7mdq9jcrr28",
"registry+https://github.com/rust-lang/crates.io-index#phf_generator@0.7.24": "0qi62gxk3x3whrmw5c4i71406icqk11qmpgln438p6qm7k4lqdh9",
"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.11.2": "0azphb0a330ypqx3qvyffal5saqnks0xvl8rj73jlk3qxxgbkz4h",
"registry+https://github.com/rust-lang/crates.io-index#phf_shared@0.7.24": "18371fla0vsj7d6d5rlfb747xbr2in11ar9vgv5qna72bnhp2kr3",
"registry+https://github.com/rust-lang/crates.io-index#pin-project-internal@1.1.3": "01a4l3vb84brv9v7wl71chzxra2kynm6yvcjca66xv3ij6fgsna3",
"registry+https://github.com/rust-lang/crates.io-index#pin-project-lite@0.2.13": "0n0bwr5qxlf0mhn2xkl36sy55118s9qmvx2yl5f3ixkb007lbywa",
"registry+https://github.com/rust-lang/crates.io-index#pin-project@1.1.3": "08k4cpy8q3j93qqgnrbzkcgpn7g0a88l4a9nm33kyghpdhffv97x",
"registry+https://github.com/rust-lang/crates.io-index#pin-utils@0.1.0": "117ir7vslsl2z1a7qzhws4pd01cg2d3338c47swjyvqv2n60v1wb",
"registry+https://github.com/rust-lang/crates.io-index#piper@0.2.1": "1m45fkdq7q5l9mv3b0ra10qwm0kb67rjp2q8y91958gbqjqk33b6",
"registry+https://github.com/rust-lang/crates.io-index#pkcs1@0.7.5": "0zz4mil3nchnxljdfs2k5ab1cjqn7kq5lqp62n9qfix01zqvkzy8",
"registry+https://github.com/rust-lang/crates.io-index#pkcs8@0.10.2": "1dx7w21gvn07azszgqd3ryjhyphsrjrmq5mmz1fbxkj5g0vv4l7r",
"registry+https://github.com/rust-lang/crates.io-index#pkg-config@0.3.27": "0r39ryh1magcq4cz5g9x88jllsnxnhcqr753islvyk4jp9h2h1r6",
"registry+https://github.com/rust-lang/crates.io-index#plugin@0.2.6": "1q7nghkpvxxr168y2jnzh3w7qc9vfrby9n7ygy3xpj0bj71hsshs",
"registry+https://github.com/rust-lang/crates.io-index#png@0.16.8": "1ipl44q3vy4kvx6j296vk7d4v8gvcg203lrkvvixwixq1j98fciw",
"registry+https://github.com/rust-lang/crates.io-index#png@0.17.10": "0r5a8a25ad0jq2pkp2zbab3wwhpgp6jmdg6d0ybjnw6kilnvyxfx",
"registry+https://github.com/rust-lang/crates.io-index#polling@2.8.0": "1kixxfq1af1k7gkmmk9yv4j2krpp4fji2r8j4cz6p6d7ihz34bab",
"registry+https://github.com/rust-lang/crates.io-index#polling@3.4.0": "052am20b5r03nwhpnjw86rv3dwsdabvb07anv3fqxfbs65r4w19h",
"registry+https://github.com/rust-lang/crates.io-index#powerfmt@0.2.0": "14ckj2xdpkhv3h6l5sdmb9f1d57z8hbfpdldjc2vl5givq2y77j3",
"registry+https://github.com/rust-lang/crates.io-index#ppv-lite86@0.2.17": "1pp6g52aw970adv3x2310n7glqnji96z0a9wiamzw89ibf0ayh2v",
"registry+https://github.com/rust-lang/crates.io-index#pretty_env_logger@0.5.0": "076w9dnvcpx6d3mdbkqad8nwnsynb7c8haxmscyrz7g3vga28mw6",
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-crate@1.3.1": "069r1k56bvgk0f58dm5swlssfcp79im230affwk6d9ck20g04k3z",
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-crate@2.0.1": "06jbv5w6s04dbjbwq0iv7zil12ildf3w8dvvb4pqvhig4gm5zp4p",
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error-attr@1.0.4": "0sgq6m5jfmasmwwy8x4mjygx5l7kp8s4j60bv25ckv2j1qc41gm1",
"registry+https://github.com/rust-lang/crates.io-index#proc-macro-error@1.0.4": "1373bhxaf0pagd8zkyd03kkx6bchzf6g0dkwrwzsnal9z47lj9fs",
"registry+https://github.com/rust-lang/crates.io-index#proc-macro2@1.0.78": "1bjak27pqdn4f4ih1c9nr3manzyavsgqmf76ygw9k76q8pb2lhp2",
"registry+https://github.com/rust-lang/crates.io-index#proptest@1.4.0": "1gzmw40pgmwzb7x6jsyr88z5w151snv5rp1g0dlcp1iw3h9pdd1i",
"registry+https://github.com/rust-lang/crates.io-index#qoi@0.4.1": "00c0wkb112annn2wl72ixyd78mf56p4lxkhlmsggx65l3v3n8vbz",
"registry+https://github.com/rust-lang/crates.io-index#quick-error@1.2.3": "1q6za3v78hsspisc197bg3g7rpc989qycy8ypr8ap8igv10ikl51",
"registry+https://github.com/rust-lang/crates.io-index#quote@1.0.35": "1vv8r2ncaz4pqdr78x7f138ka595sp2ncr1sa2plm4zxbsmwj7i9",
"registry+https://github.com/rust-lang/crates.io-index#rand@0.3.23": "0v679h38pjjqj5h4md7v2slsvj6686qgcn7p9fbw3h43iwnk1b34",
"registry+https://github.com/rust-lang/crates.io-index#rand@0.4.6": "14qjfv3gggzhnma20k0sc1jf8y6pplsaq7n1j9ls5c8kf2wl0a2m",
"registry+https://github.com/rust-lang/crates.io-index#rand@0.6.5": "1jl4449jcl4wgmzld6ffwqj5gwxrp8zvx8w573g1z368qg6xlwbd",
"registry+https://github.com/rust-lang/crates.io-index#rand@0.8.5": "013l6931nn7gkc23jz5mm3qdhf93jjf0fg64nz2lp4i51qd8vbrl",
"registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.1.1": "1vxwyzs4fy1ffjc8l00fsyygpiss135irjf7nyxgq2v0lqf3lvam",
"registry+https://github.com/rust-lang/crates.io-index#rand_chacha@0.3.1": "123x2adin558xbhvqb8w4f6syjsdkmqff8cxwhmjacpsl1ihmhg6",
"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.3.1": "0jzdgszfa4bliigiy4hi66k7fs3gfwi2qxn8vik84ph77fwdwvvs",
"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.4.2": "1p09ynysrq1vcdlmcqnapq4qakl2yd1ng3kxh3qscpx09k2a6cww",
"registry+https://github.com/rust-lang/crates.io-index#rand_core@0.6.4": "0b4j2v4cb5krak1pv6kakv4sz6xcwbrmy2zckc32hsigbrwy82zc",
"registry+https://github.com/rust-lang/crates.io-index#rand_hc@0.1.0": "1i0vl8q5ddvvy0x8hf1zxny393miyzxkwqnw31ifg6p0gdy6fh3v",
"registry+https://github.com/rust-lang/crates.io-index#rand_isaac@0.1.1": "027flpjr4znx2csxk7gxb7vrf9c7y5mydmvg5az2afgisp4rgnfy",
"registry+https://github.com/rust-lang/crates.io-index#rand_jitter@0.1.4": "16z387y46bfz3csc42zxbjq89vcr1axqacncvv8qhyy93p4xarhi",
"registry+https://github.com/rust-lang/crates.io-index#rand_os@0.1.3": "0wahppm0s64gkr2vmhcgwc0lij37in1lgfxg5rbgqlz0l5vgcxbv",
"registry+https://github.com/rust-lang/crates.io-index#rand_pcg@0.1.2": "0i0bdla18a8x4jn1w0fxsbs3jg7ajllz6azmch1zw33r06dv1ydb",
"registry+https://github.com/rust-lang/crates.io-index#rand_xorshift@0.1.1": "0p2x8nr00hricpi2m6ca5vysiha7ybnghz79yqhhx6sl4gkfkxyb",
"registry+https://github.com/rust-lang/crates.io-index#rand_xorshift@0.3.0": "13vcag7gmqspzyabfl1gr9ykvxd2142q2agrj8dkyjmfqmgg4nyj",
"registry+https://github.com/rust-lang/crates.io-index#rayon-core@1.12.0": "1vaq0q71yfvcwlmia0iqf6ixj2fibjcf2xjy92n1m1izv1mgpqsw",
"registry+https://github.com/rust-lang/crates.io-index#rayon@1.8.0": "1cfdnvchf7j4cpha5jkcrrsr61li9i9lp5ak7xdq6d3pvc1xn9ww",
"registry+https://github.com/rust-lang/crates.io-index#rdrand@0.4.0": "1cjq0kwx1bk7jx3kzyciiish5gqsj7620dm43dc52sr8fzmm9037",
"registry+https://github.com/rust-lang/crates.io-index#redox_syscall@0.4.1": "1aiifyz5dnybfvkk4cdab9p2kmphag1yad6iknc7aszlxxldf8j7",
"registry+https://github.com/rust-lang/crates.io-index#regex-automata@0.4.3": "0gs8q9yhd3kcg4pr00ag4viqxnh5l7jpyb9fsfr8hzh451w4r02z",
"registry+https://github.com/rust-lang/crates.io-index#regex-syntax@0.8.2": "17rd2s8xbiyf6lb4aj2nfi44zqlj98g2ays8zzj2vfs743k79360",
"registry+https://github.com/rust-lang/crates.io-index#regex@1.10.2": "0hxkd814n4irind8im5c9am221ri6bprx49nc7yxv02ykhd9a2rq",
"registry+https://github.com/rust-lang/crates.io-index#remove_dir_all@0.5.3": "1rzqbsgkmr053bxxl04vmvsd1njyz0nxvly97aip6aa2cmb15k9s",
"registry+https://github.com/rust-lang/crates.io-index#reqwest@0.11.23": "0hgvzb7r46656r9vqhl5qk1kbr2xzjb91yr2cb321160ka6sxc9p",
"registry+https://github.com/rust-lang/crates.io-index#rsa@0.9.6": "1z0d1aavfm0v4pv8jqmqhhvvhvblla1ydzlvwykpc3mkzhj523jx",
"registry+https://github.com/rust-lang/crates.io-index#rustc-demangle@0.1.23": "0xnbk2bmyzshacjm2g1kd4zzv2y2az14bw3sjccq5qkpmsfvn9nn",
"registry+https://github.com/rust-lang/crates.io-index#rustc-hash@1.1.0": "1qkc5khrmv5pqi5l5ca9p5nl5hs742cagrndhbrlk3dhlrx3zm08",
"registry+https://github.com/rust-lang/crates.io-index#rustc_version@0.4.0": "0rpk9rcdk405xhbmgclsh4pai0svn49x35aggl4nhbkd4a2zb85z",
"registry+https://github.com/rust-lang/crates.io-index#rustix@0.37.27": "1lidfswa8wbg358yrrkhfvsw0hzlvl540g4lwqszw09sg8vcma7y",
"registry+https://github.com/rust-lang/crates.io-index#rustix@0.38.28": "05m3vacvbqbg6r6ksmx9k5afpi0lppjdv712crrpsrfax2jp5rbj",
"registry+https://github.com/rust-lang/crates.io-index#rustls-pemfile@1.0.4": "1324n5bcns0rnw6vywr5agff3rwfvzphi7rmbyzwnv6glkhclx0w",
"registry+https://github.com/rust-lang/crates.io-index#rusty-fork@0.3.0": "0kxwq5c480gg6q0j3bg4zzyfh2kwmc3v2ba94jw8ncjc8mpcqgfb",
"registry+https://github.com/rust-lang/crates.io-index#ryu@1.0.16": "0k7b90xr48ag5bzmfjp82rljasw2fx28xr3bg1lrpx7b5sljm3gr",
"registry+https://github.com/rust-lang/crates.io-index#safemem@0.3.3": "0wp0d2b2284lw11xhybhaszsczpbq1jbdklkxgifldcknmy3nw7g",
"registry+https://github.com/rust-lang/crates.io-index#schannel@0.1.22": "126zy5jb95fc5hvzyjwiq6lc81r08rdcn6affn00ispp9jzk6dqc",
"registry+https://github.com/rust-lang/crates.io-index#scoped-tls@1.0.1": "15524h04mafihcvfpgxd8f4bgc3k95aclz8grjkg9a0rxcvn9kz1",
"registry+https://github.com/rust-lang/crates.io-index#scoped_threadpool@0.1.9": "1a26d3lk40s9mrf4imhbik7caahmw2jryhhb6vqv6fplbbgzal8x",
"registry+https://github.com/rust-lang/crates.io-index#scopeguard@1.2.0": "0jcz9sd47zlsgcnm1hdw0664krxwb5gczlif4qngj2aif8vky54l",
"registry+https://github.com/rust-lang/crates.io-index#security-framework-sys@2.9.1": "0yhciwlsy9dh0ps1gw3197kvyqx1bvc4knrhiznhid6kax196cp9",
"registry+https://github.com/rust-lang/crates.io-index#security-framework@2.9.2": "1pplxk15s5yxvi2m1sz5xfmjibp96cscdcl432w9jzbk0frlzdh5",
"registry+https://github.com/rust-lang/crates.io-index#self_cell@0.10.3": "0pci3zh23b7dg6jmlxbn8k4plb7hcg5jprd1qiz0rp04p1ilskp1",
"registry+https://github.com/rust-lang/crates.io-index#self_cell@1.0.2": "1rmdglwnd77wcw2gv76finpgzjhkynx422d0jpahrf2fsqn37273",
"registry+https://github.com/rust-lang/crates.io-index#semver@1.0.20": "140hmbfa743hbmah1zjf07s8apavhvn04204qjigjiz5w6iscvw3",
"registry+https://github.com/rust-lang/crates.io-index#serde@0.9.15": "1bsla8l5xr9pp5sirkal6mngxcq6q961km88jvf339j5ff8j7dil",
"registry+https://github.com/rust-lang/crates.io-index#serde@1.0.193": "129b0j67594f8qg5cbyi3nyk31y97wrqihi026mba34dwrsrkp95",
"registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.193": "1lwlx2k7wxr1v160kpyqjfabs37gm1yxqg65383rnyrm06jnqms3",
"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.108": "0ssj59s7lpzqh1m50kfzlnrip0p0jg9lmhn4098i33a0mhz7w71x",
"registry+https://github.com/rust-lang/crates.io-index#serde_spanned@0.6.5": "1hgh6s3jjwyzhfk3xwb6pnnr1misq9nflwq0f026jafi37s24dpb",
"registry+https://github.com/rust-lang/crates.io-index#serde_urlencoded@0.7.1": "1zgklbdaysj3230xivihs30qi5vkhigg323a9m62k8jwf4a1qjfk",
"registry+https://github.com/rust-lang/crates.io-index#sha1@0.10.6": "1fnnxlfg08xhkmwf2ahv634as30l1i3xhlhkvxflmasi5nd85gz3",
"registry+https://github.com/rust-lang/crates.io-index#sha2@0.10.8": "1j1x78zk9il95w9iv46dh9wm73r6xrgj32y6lzzw7bxws9dbfgbr",
"registry+https://github.com/rust-lang/crates.io-index#signal-hook-registry@1.4.1": "18crkkw5k82bvcx088xlf5g4n3772m24qhzgfan80nda7d3rn8nq",
"registry+https://github.com/rust-lang/crates.io-index#signature@2.2.0": "1pi9hd5vqfr3q3k49k37z06p7gs5si0in32qia4mmr1dancr6m3p",
"registry+https://github.com/rust-lang/crates.io-index#simd-adler32@0.3.7": "1zkq40c3iajcnr5936gjp9jjh1lpzhy44p3dq3fiw75iwr1w2vfn",
"registry+https://github.com/rust-lang/crates.io-index#siphasher@0.2.3": "1b53m53l24lyhr505lwqzrpjyq5qfnic71mynrcfvm43rybf938b",
"registry+https://github.com/rust-lang/crates.io-index#siphasher@0.3.11": "03axamhmwsrmh0psdw3gf7c0zc4fyl5yjxfifz9qfka6yhkqid9q",
"registry+https://github.com/rust-lang/crates.io-index#slab@0.4.9": "0rxvsgir0qw5lkycrqgb1cxsvxzjv9bmx73bk5y42svnzfba94lg",
"registry+https://github.com/rust-lang/crates.io-index#smallvec@1.11.2": "0w79x38f7c0np7hqfmzrif9zmn0avjvvm31b166zdk9d1aad1k2d",
"registry+https://github.com/rust-lang/crates.io-index#snowflake@1.3.0": "1wadr7bxdxbmkbqkqsvzan6q1h3mxqpxningi3ss3v9jaav7n817",
"registry+https://github.com/rust-lang/crates.io-index#socket2@0.4.10": "03ack54dxhgfifzsj14k7qa3r5c9wqy3v6mqhlim99cc03y1cycz",
"registry+https://github.com/rust-lang/crates.io-index#socket2@0.5.5": "1sgq315f1njky114ip7wcy83qlphv9qclprfjwvxcpfblmcsqpvv",
"registry+https://github.com/rust-lang/crates.io-index#spin@0.5.2": "0b84m6dbzrwf2kxylnw82d3dr8w06av7rfkr8s85fb5f43rwyqvf",
"registry+https://github.com/rust-lang/crates.io-index#spin@0.9.8": "0rvam5r0p3a6qhc18scqpvpgb3ckzyqxpgdfyjnghh8ja7byi039",
"registry+https://github.com/rust-lang/crates.io-index#spki@0.7.3": "17fj8k5fmx4w9mp27l970clrh5qa7r5sjdvbsln987xhb34dc7nr",
"registry+https://github.com/rust-lang/crates.io-index#sqlformat@0.2.3": "0v0p70wjdshj18zgjjac9xlx8hmpx33xhq7g8x9rg4s4gjyvg0ff",
"registry+https://github.com/rust-lang/crates.io-index#sqlx-core@0.7.3": "1gdz44yb9qwxv4xl4hv6w4vbqx0zzdlzsf9j9gcj1qir6wy0ljyq",
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros-core@0.7.3": "0h88wahkxa6nam536lhwr1y0yxlr6la8b1x0hs0n88v790clbgfh",
"registry+https://github.com/rust-lang/crates.io-index#sqlx-macros@0.7.3": "19gjwisiym07q7ibkp9nkvvbywjh0r5rc572msvzyzadvh01r5l9",
"registry+https://github.com/rust-lang/crates.io-index#sqlx-mysql@0.7.3": "190ygz5a3pqcd9vvqjv2i4r1xh8vi53j4272yrld07zpblwrawg3",
"registry+https://github.com/rust-lang/crates.io-index#sqlx-postgres@0.7.3": "090wm9s6mm53ggn1xwr183cnn8yxly8rgcksdk4hrlfcnz1hmb6n",
"registry+https://github.com/rust-lang/crates.io-index#sqlx-sqlite@0.7.3": "143laha7wf8dmi0xwycwqmvxdcnb25dq7jnqrsgvmis8v6vpc291",
"registry+https://github.com/rust-lang/crates.io-index#sqlx@0.7.3": "1kv3hyx7izmmsjqh3l47zrfhjlcblpg20cvnk7pr8dm7klkkr86v",
"registry+https://github.com/rust-lang/crates.io-index#stringprep@0.1.4": "1rkfsf7riynsmqj3hbldfrvmna0i9chx2sz39qdpl40s4d7dfhdv",
"registry+https://github.com/rust-lang/crates.io-index#strsim@0.10.0": "08s69r4rcrahwnickvi0kq49z524ci50capybln83mg6b473qivk",
"registry+https://github.com/rust-lang/crates.io-index#subtle@2.5.0": "1g2yjs7gffgmdvkkq0wrrh0pxds3q0dv6dhkw9cdpbib656xdkc1",
"registry+https://github.com/rust-lang/crates.io-index#syn@1.0.109": "0ds2if4600bd59wsv7jjgfkayfzy3hnazs394kz6zdkmna8l3dkj",
"registry+https://github.com/rust-lang/crates.io-index#syn@2.0.48": "0gqgfygmrxmp8q32lia9p294kdd501ybn6kn2h4gqza0irik2d8g",
"registry+https://github.com/rust-lang/crates.io-index#system-configuration-sys@0.5.0": "1jckxvdr37bay3i9v52izgy52dg690x5xfg3hd394sv2xf4b2px7",
"registry+https://github.com/rust-lang/crates.io-index#system-configuration@0.5.1": "1rz0r30xn7fiyqay2dvzfy56cvaa3km74hnbz2d72p97bkf3lfms",
"registry+https://github.com/rust-lang/crates.io-index#system-deps@6.2.0": "0c836abhh3k8yn5ymg8wx383ay7n731gkrbbp3gma352yq7mhb9a",
"registry+https://github.com/rust-lang/crates.io-index#target-lexicon@0.12.12": "02lk65ik5ffb8vl9qzq02v0df8kxrp16zih78a33mji49789zhql",
"registry+https://github.com/rust-lang/crates.io-index#tempdir@0.3.7": "1n5n86zxpgd85y0mswrp5cfdisizq2rv3la906g6ipyc03xvbwhm",
"registry+https://github.com/rust-lang/crates.io-index#tempfile@3.8.1": "1r88v07zdafzf46y63vs39rmzwl4vqd4g2c5qarz9mqa8nnavwby",
"registry+https://github.com/rust-lang/crates.io-index#termcolor@1.4.0": "0jfllflbxxffghlq6gx4csv0bv0qv77943dcx01h9zssy39w66zz",
"registry+https://github.com/rust-lang/crates.io-index#thiserror-impl@1.0.51": "1ps9ylhlk2vn19fv3cxp40j3wcg1xmb117g2z2fbf4vmg2bj4x01",
"registry+https://github.com/rust-lang/crates.io-index#thiserror@1.0.51": "1drvyim21w5sga3izvnvivrdp06l2c24xwbhp0vg1mhn2iz2277i",
"registry+https://github.com/rust-lang/crates.io-index#tiff@0.6.1": "0ds48vs919ccxa3fv1www7788pzkvpg434ilqkq7sjb5dmqg8lws",
"registry+https://github.com/rust-lang/crates.io-index#tiff@0.9.0": "04b2fd3clxm0pmdlfip8xj594zyrsfwmh641i6x1gfiz9l7jn5vd",
"registry+https://github.com/rust-lang/crates.io-index#time-core@0.1.2": "1wx3qizcihw6z151hywfzzyd1y5dl804ydyxci6qm07vbakpr4pg",
"registry+https://github.com/rust-lang/crates.io-index#time-macros@0.2.16": "0gx4ngf5g7ydqa8lf7kh9sy72rd4dhvpi31y1jvswi0288rpw696",
"registry+https://github.com/rust-lang/crates.io-index#time@0.1.45": "0nl0pzv9yf56djy8y5dx25nka5pr2q1ivlandb3d24pksgx7ly8v",
"registry+https://github.com/rust-lang/crates.io-index#time@0.3.31": "0gjqcdsdbh0r5vi4c2vrj5a6prdviapx731wwn07cvpqqd1blmzn",
"registry+https://github.com/rust-lang/crates.io-index#tinystr@0.7.5": "1khf3j95bwwksj2hw76nlvwlwpwi4d1j421lj6x35arqqprjph43",
"registry+https://github.com/rust-lang/crates.io-index#tinyvec@1.6.0": "0l6bl2h62a5m44jdnpn7lmj14rd44via8180i7121fvm73mmrk47",
"registry+https://github.com/rust-lang/crates.io-index#tinyvec_macros@0.1.1": "081gag86208sc3y6sdkshgw3vysm5d34p431dzw0bshz66ncng0z",
"registry+https://github.com/rust-lang/crates.io-index#tokio-macros@2.2.0": "0fwjy4vdx1h9pi4g2nml72wi0fr27b5m954p13ji9anyy8l1x2jv",
"registry+https://github.com/rust-lang/crates.io-index#tokio-native-tls@0.3.1": "1wkfg6zn85zckmv4im7mv20ca6b1vmlib5xwz9p7g19wjfmpdbmv",
"registry+https://github.com/rust-lang/crates.io-index#tokio-stream@0.1.14": "0hi8hcwavh5sdi1ivc9qc4yvyr32f153c212dpd7sb366y6rhz1r",
"registry+https://github.com/rust-lang/crates.io-index#tokio-tungstenite@0.20.1": "0v1v24l27hxi5hlchs7hfd5rgzi167x0ygbw220nvq0w5b5msb91",
"registry+https://github.com/rust-lang/crates.io-index#tokio-util@0.7.10": "058y6x4mf0fsqji9rfyb77qbfyc50y4pk2spqgj6xsyr693z66al",
"registry+https://github.com/rust-lang/crates.io-index#tokio@1.35.1": "01613rkziqp812a288ga65aqygs254wgajdi57v8brivjkx4x6y8",
"registry+https://github.com/rust-lang/crates.io-index#toml@0.8.2": "0g9ysjaqvm2mv8q85xpqfn7hi710hj24sd56k49wyddvvyq8lp8q",
"registry+https://github.com/rust-lang/crates.io-index#toml_datetime@0.6.3": "0jsy7v8bdvmzsci6imj8fzgd255fmy5fzp6zsri14yrry7i77nkw",
"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.19.15": "08bl7rp5g6jwmfpad9s8jpw8wjrciadpnbaswgywpr9hv9qbfnqv",
"registry+https://github.com/rust-lang/crates.io-index#toml_edit@0.20.2": "0f7k5svmxw98fhi28jpcyv7ldr2s3c867pjbji65bdxjpd44svir",
"registry+https://github.com/rust-lang/crates.io-index#tower-service@0.3.2": "0lmfzmmvid2yp2l36mbavhmqgsvzqf7r2wiwz73ml4xmwaf1rg5n",
"registry+https://github.com/rust-lang/crates.io-index#tracing-attributes@0.1.27": "1rvb5dn9z6d0xdj14r403z0af0bbaqhg02hq4jc97g5wds6lqw1l",
"registry+https://github.com/rust-lang/crates.io-index#tracing-core@0.1.32": "0m5aglin3cdwxpvbg6kz0r9r0k31j48n0kcfwsp6l49z26k3svf0",
"registry+https://github.com/rust-lang/crates.io-index#tracing@0.1.40": "1vv48dac9zgj9650pg2b4d0j3w6f3x9gbggf43scq5hrlysklln3",
"registry+https://github.com/rust-lang/crates.io-index#traitobject@0.1.0": "0yb0n8822mr59j200fyr2fxgzzgqljyxflx9y8bdy3rlaqngilgg",
"registry+https://github.com/rust-lang/crates.io-index#try-lock@0.2.5": "0jqijrrvm1pyq34zn1jmy2vihd4jcrjlvsh4alkjahhssjnsn8g4",
"registry+https://github.com/rust-lang/crates.io-index#tungstenite@0.20.1": "1fbgcv3h4h1bhhf5sqbwqsp7jnc44bi4m41sgmhzdsk2zl8aqgcy",
"registry+https://github.com/rust-lang/crates.io-index#type-map@0.4.0": "0ilsqq7pcl3k9ggxv2x5fbxxfd6x7ljsndrhc38jmjwnbr63dlxn",
"registry+https://github.com/rust-lang/crates.io-index#typeable@0.1.2": "11w8dywgnm32hb291izjvh4zjd037ccnkk77ahk63l913zwzc40l",
"registry+https://github.com/rust-lang/crates.io-index#typemap@0.3.3": "1xm1gbvz9qisj1l6d36hrl9pw8imr8ngs6qyanjnsad3h0yfcfv5",
"registry+https://github.com/rust-lang/crates.io-index#typenum@1.17.0": "09dqxv69m9lj9zvv6xw5vxaqx15ps0vxyy5myg33i0kbqvq0pzs2",
"registry+https://github.com/rust-lang/crates.io-index#typeshare-annotation@1.0.2": "1adpfhyz3lqjjbq2ym69mv62ymqyd5651gxlqdy8aa446l70srzw",
"registry+https://github.com/rust-lang/crates.io-index#typeshare@1.0.1": "1mi7snkx2b4g84x8vx38v1myg5r6g48c865j0nz5zcsc8lpilkgl",
"registry+https://github.com/rust-lang/crates.io-index#unarray@0.1.4": "154smf048k84prsdgh09nkm2n0w0336v84jd4zikyn6v6jrqbspa",
"registry+https://github.com/rust-lang/crates.io-index#unic-langid-impl@0.9.4": "1ijvqmsrg6qw3b1h9bh537pvwk2jn2kl6ck3z3qlxspxcch5mmab",
"registry+https://github.com/rust-lang/crates.io-index#unic-langid@0.9.4": "05pm5p3j29c9jw9a4dr3v64g3x6g3zh37splj47i7vclszk251r3",
"registry+https://github.com/rust-lang/crates.io-index#unicase@1.4.2": "0cwazh4qsmm9msckjk86zc1z35xg7hjxjykrgjalzdv367w6aivz",
"registry+https://github.com/rust-lang/crates.io-index#unicase@2.7.0": "12gd74j79f94k4clxpf06l99wiv4p30wjr0qm04ihqk9zgdd9lpp",
"registry+https://github.com/rust-lang/crates.io-index#unicode-bidi@0.3.14": "05i4ps31vskq1wdp8yf315fxivyh1frijly9d4gb5clygbr2h9bg",
"registry+https://github.com/rust-lang/crates.io-index#unicode-ident@1.0.12": "0jzf1znfpb2gx8nr8mvmyqs1crnv79l57nxnbiszc7xf7ynbjm1k",
"registry+https://github.com/rust-lang/crates.io-index#unicode-normalization@0.1.22": "08d95g7b1irc578b2iyhzv4xhsa4pfvwsqxcl9lbcpabzkq16msw",
"registry+https://github.com/rust-lang/crates.io-index#unicode-segmentation@1.10.1": "0dky2hm5k51xy11hc3nk85p533rvghd462b6i0c532b7hl4j9mhx",
"registry+https://github.com/rust-lang/crates.io-index#unicode_categories@0.1.1": "0kp1d7fryxxm7hqywbk88yb9d1avsam9sg76xh36k5qx2arj9v1r",
"registry+https://github.com/rust-lang/crates.io-index#unsafe-any@0.4.2": "0zwwphsqkw5qaiqmjwngnfpv9ym85qcsyj7adip9qplzjzbn00zk",
"registry+https://github.com/rust-lang/crates.io-index#url@1.7.2": "0nim1c90mxpi9wgdw2xh8dqd72vlklwlzam436akcrhjac6pqknx",
"registry+https://github.com/rust-lang/crates.io-index#url@2.5.0": "0cs65961miawncdg2z20171w0vqrmraswv2ihdpd8lxp7cp31rii",
"registry+https://github.com/rust-lang/crates.io-index#urlencoding@2.1.3": "1nj99jp37k47n0hvaz5fvz7z6jd0sb4ppvfy3nphr1zbnyixpy6s",
"registry+https://github.com/rust-lang/crates.io-index#utf-8@0.7.6": "1a9ns3fvgird0snjkd3wbdhwd3zdpc2h5gpyybrfr6ra5pkqxk09",
"registry+https://github.com/rust-lang/crates.io-index#utf8parse@0.2.1": "02ip1a0az0qmc2786vxk2nqwsgcwf17d3a38fkf0q7hrmwh9c6vi",
"registry+https://github.com/rust-lang/crates.io-index#uuid@0.4.0": "0cdj2v6v2yy3zyisij69waksd17cyir1n58kwyk1n622105wbzkw",
"registry+https://github.com/rust-lang/crates.io-index#uuid@0.8.2": "1dy4ldcp7rnzjy56dxh7d2sgrcvn4q77y0a8r0a48946h66zjp5w",
"registry+https://github.com/rust-lang/crates.io-index#uuid@1.6.1": "0q45jxahvysldn3iy04m8xmr8hgig80855y9gq9di8x72v7myfay",
"registry+https://github.com/rust-lang/crates.io-index#value-bag@1.7.0": "02r8wccrzi3bzlkrslkcfw9pwp8kwif9szif2i9arn9dzqx44vhj",
"registry+https://github.com/rust-lang/crates.io-index#vcpkg@0.2.15": "09i4nf5y8lig6xgj3f7fyrvzd3nlaw4znrihw8psidvv5yk4xkdc",
"registry+https://github.com/rust-lang/crates.io-index#version-compare@0.1.1": "0acg4pmjdbmclg0m7yhijn979mdy66z3k8qrcnvn634f1gy456jp",
"registry+https://github.com/rust-lang/crates.io-index#version_check@0.1.5": "1pf91pvj8n6akh7w6j5ypka6aqz08b3qpzgs0ak2kjf4frkiljwi",
"registry+https://github.com/rust-lang/crates.io-index#version_check@0.9.4": "0gs8grwdlgh0xq660d7wr80x14vxbizmd8dbp29p2pdncx8lp1s9",
"registry+https://github.com/rust-lang/crates.io-index#wait-timeout@0.2.0": "1xpkk0j5l9pfmjfh1pi0i89invlavfrd9av5xp0zhxgb29dhy84z",
"registry+https://github.com/rust-lang/crates.io-index#waker-fn@1.1.1": "142n74wlmpwcazfb5v7vhnzj3lb3r97qy8mzpjdpg345aizm3i7k",
"registry+https://github.com/rust-lang/crates.io-index#want@0.3.1": "03hbfrnvqqdchb5kgxyavb9jabwza0dmh2vw5kg0dq8rxl57d9xz",
"registry+https://github.com/rust-lang/crates.io-index#warp@0.3.6": "0sfimrpxkyka1mavfhg5wa4x977qs8vyxa510c627w9zw0i2xsf1",
"registry+https://github.com/rust-lang/crates.io-index#wasi@0.10.0+wasi-snapshot-preview1": "07y3l8mzfzzz4cj09c8y90yak4hpsi9g7pllyzpr6xvwrabka50s",
"registry+https://github.com/rust-lang/crates.io-index#wasi@0.11.0+wasi-snapshot-preview1": "08z4hxwkpdpalxjps1ai9y7ihin26y9f476i53dv98v45gkqg3cw",
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-backend@0.2.89": "09l8lyylsdssz993h4fzja69zpvpykaw84fivs210fjgwqjzcmhv",
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-futures@0.4.39": "04lsxpw4jqfwh7c0crzx0smj52nvwp1w3bh4098sq90149da2dmc",
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro-support@0.2.89": "10sj1gr2naxv5q116yjb929hhpvz45dxbkvyk8hyc2lknzy85szh",
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-macro@0.2.89": "1cl2w7k5jn2jbd5kx613c8k8vjvda22hfgcgx7y2mk93fbrxnqh1",
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen-shared@0.2.89": "17s5rppad113c6ggkaq8c3cg7a3zz15i78wxcg6mcl1n15iv7fbs",
"registry+https://github.com/rust-lang/crates.io-index#wasm-bindgen@0.2.89": "0kh6akdldy13z9xqj0skz6b4npq1d98bjkgzb8ccq59hibvd9l0f",
"registry+https://github.com/rust-lang/crates.io-index#web-sys@0.3.66": "03q1z22djv5ncqkyydcvnchmdsl5gvnyzcyixkxnifw6xi24mhjh",
"registry+https://github.com/rust-lang/crates.io-index#weezl@0.1.7": "1frdbq6y5jn2j93i20hc80swpkj30p1wffwxj1nr4fp09m6id4wi",
"registry+https://github.com/rust-lang/crates.io-index#whoami@1.4.1": "0l6ca9pl92wmngsn1dh9ih716v216nmn2zvcn94k04x9p1b3gz12",
"registry+https://github.com/rust-lang/crates.io-index#winapi-i686-pc-windows-gnu@0.4.0": "1dmpa6mvcvzz16zg6d5vrfy4bxgg541wxrcip7cnshi06v38ffxc",
"registry+https://github.com/rust-lang/crates.io-index#winapi-util@0.1.6": "15i5lm39wd44004i9d5qspry2cynkrpvwzghr6s2c3dsk28nz7pj",
"registry+https://github.com/rust-lang/crates.io-index#winapi-x86_64-pc-windows-gnu@0.4.0": "0gqq64czqb64kskjryj8isp62m2sgvx25yyj3kpc2myh85w24bki",
"registry+https://github.com/rust-lang/crates.io-index#winapi@0.3.9": "06gl025x418lchw1wxj64ycr7gha83m44cjr5sarhynd9xkrm0sw",
"registry+https://github.com/rust-lang/crates.io-index#windows-core@0.51.1": "0r1f57hsshsghjyc7ypp2s0i78f7b1vr93w68sdb8baxyf2czy7i",
"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.48.0": "1aan23v5gs7gya1lc46hqn9mdh8yph3fhxmhxlw36pn6pqc28zb7",
"registry+https://github.com/rust-lang/crates.io-index#windows-sys@0.52.0": "0gd3v4ji88490zgb6b5mq5zgbvwv7zx1ibn8v3x83rwcdbryaar8",
"registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.48.5": "034ljxqshifs1lan89xwpcy1hp0lhdh4b5n0d2z4fwjx2piacbws",
"registry+https://github.com/rust-lang/crates.io-index#windows-targets@0.52.0": "1kg7a27ynzw8zz3krdgy6w5gbqcji27j1sz4p7xk2j5j8082064a",
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.48.5": "1n05v7qblg1ci3i567inc7xrkmywczxrs1z3lj3rkkxw18py6f1b",
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_gnullvm@0.52.0": "1shmn1kbdc0bpphcxz0vlph96bxz0h1jlmh93s9agf2dbpin8xyb",
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.48.5": "1g5l4ry968p73g6bg6jgyvy9lb8fyhcs54067yzxpcpkf44k2dfw",
"registry+https://github.com/rust-lang/crates.io-index#windows_aarch64_msvc@0.52.0": "1vvmy1ypvzdvxn9yf0b8ygfl85gl2gpcyvsvqppsmlpisil07amv",
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.48.5": "0gklnglwd9ilqx7ac3cn8hbhkraqisd0n83jxzf9837nvvkiand7",
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_gnu@0.52.0": "04zkglz4p3pjsns5gbz85v4s5aw102raz4spj4b0lmm33z5kg1m2",
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.48.5": "01m4rik437dl9rdf0ndnm2syh10hizvq0dajdkv2fjqcywrw4mcg",
"registry+https://github.com/rust-lang/crates.io-index#windows_i686_msvc@0.52.0": "16kvmbvx0vr0zbgnaz6nsks9ycvfh5xp05bjrhq65kj623iyirgz",
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.48.5": "13kiqqcvz2vnyxzydjh73hwgigsdr2z1xpzx313kxll34nyhmm2k",
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnu@0.52.0": "1zdy4qn178sil5sdm63lm7f0kkcjg6gvdwmcprd2yjmwn8ns6vrx",
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.48.5": "1k24810wfbgz8k48c2yknqjmiigmql6kk3knmddkv8k8g1v54yqb",
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_gnullvm@0.52.0": "17lllq4l2k1lqgcnw1cccphxp9vs7inq99kjlm2lfl9zklg7wr8s",
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.48.5": "0f4mdp895kkjh9zv8dxvn4pc10xr7839lf5pa9l0193i2pkgr57d",
"registry+https://github.com/rust-lang/crates.io-index#windows_x86_64_msvc@0.52.0": "012wfq37f18c09ij5m6rniw7xxn5fcvrxbqd0wd8vgnl3hfn9yfz",
"registry+https://github.com/rust-lang/crates.io-index#winnow@0.5.30": "1ifj9vnqna5qp0d7nb9mrinzf8j7zi1m0gv75870vm91jyw3sp4v",
"registry+https://github.com/rust-lang/crates.io-index#winreg@0.50.0": "1cddmp929k882mdh6i9f2as848f13qqna6czwsqzkh1pqnr5fkjj",
"registry+https://github.com/rust-lang/crates.io-index#zerocopy-derive@0.7.31": "06k0zk4x4n9s1blgxmxqb1g81y8q334aayx61gyy6v9y1dajkhdk",
"registry+https://github.com/rust-lang/crates.io-index#zerocopy@0.7.31": "0gcfyrmlrhmsz16qxjp2qzr6vixyaw1p04zl28f08lxkvfz62h0w",
"registry+https://github.com/rust-lang/crates.io-index#zeroize@1.7.0": "0bfvby7k9pdp6623p98yz2irqnamcyzpn7zh20nqmdn68b0lwnsj",
"registry+https://github.com/rust-lang/crates.io-index#zune-inflate@0.2.54": "00kg24jh3zqa3i6rg6yksnb71bch9yi1casqydl00s7nw8pk7avk"
}

View File

@ -45,6 +45,7 @@
pkgs.udev
pkgs.wasm-pack
typeshare.packages."x86_64-linux".default
pkgs.nodePackages_latest.typescript-language-server
];
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
ENV = "dev";

13
gm-dash/server/Cargo.toml Normal file
View File

@ -0,0 +1,13 @@
[package]
name = "gm-dash"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
pipewire = "0.8.0"
serde = { version = "1.0.209", features = ["alloc", "derive"] }
serde_json = "1.0.127"
tokio = { version = "1.39.3", features = ["full"] }
warp = "0.3.7"

View File

@ -0,0 +1,25 @@
use pipewire::{context::Context, main_loop::MainLoop};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mainloop = MainLoop::new(None)?;
let context = Context::new(&mainloop)?;
let core = context.connect(None)?;
let registry = core.get_registry()?;
let _listener = registry
.add_listener_local()
.global(|global| {
if global.props.and_then(|p| p.get("media.class")) == Some("Audio/Sink"){
println!(
"\t{:?} {:?}",
global.props.and_then(|p| p.get("node.description")),
global.props.and_then(|p| p.get("media.class"))
);
}
})
.register();
mainloop.run();
Ok(())
}

109
gm-dash/server/src/main.rs Normal file
View File

@ -0,0 +1,109 @@
use pipewire::{context::Context, main_loop::MainLoop};
use std::{
net::{Ipv6Addr, SocketAddrV6},
sync::{Arc, RwLock},
};
use tokio::task::spawn_blocking;
use warp::{serve, Filter};
struct State_ {
device_list: Vec<String>,
}
#[derive(Clone)]
struct State {
internal: Arc<RwLock<State_>>,
}
impl State {
fn new() -> State {
let internal = State_ {
device_list: vec![],
};
State {
internal: Arc::new(RwLock::new(internal)),
}
}
fn add_audio(&self, device: String) {
let mut st = self.internal.write().unwrap();
(*st).device_list.push(device);
}
fn audio_devices(&self) -> Vec<String> {
let st = self.internal.read().unwrap();
(*st).device_list.clone()
}
}
impl Default for State {
fn default() -> State {
State::new()
}
}
async fn server_main(state: State) {
let localhost: Ipv6Addr = "::1".parse().unwrap();
let server_addr = SocketAddrV6::new(localhost, 3001, 0, 0);
let root = warp::path!().map(|| "ok".to_string());
let list_output_devices = warp::path!("output_devices").map({
let state = state.clone();
move || {
let devices = state.audio_devices();
serde_json::to_string(&devices).unwrap()
}
});
let routes = root.or(list_output_devices);
serve(routes).run(server_addr).await;
}
fn handle_add_audio_device(state: State, props: &pipewire::spa::utils::dict::DictRef)
{
if props.get("media.class") == Some("Audio/Sink") {
if let Some(device_name) = props.get("node.description") {
state.add_audio(device_name.to_owned());
}
}
}
fn pipewire_loop(state: State) -> Result<(), Box<dyn std::error::Error>> {
let mainloop = MainLoop::new(None)?;
let context = Context::new(&mainloop)?;
let core = context.connect(None)?;
let registry = core.get_registry()?;
let _listener = registry
.add_listener_local()
.global({
let state = state.clone();
move |global_data| {
if let Some(props) = global_data.props {
handle_add_audio_device(state.clone(), props);
}
}
})
.register();
mainloop.run();
Ok(())
}
fn pipewire_main(state: State) {
pipewire_loop(state).expect("pipewire should not error");
}
#[tokio::main]
async fn main() {
let state = State::default();
spawn_blocking({
let state = state.clone();
move || pipewire_main(state)
});
server_main(state.clone()).await;
}

23
gm-dash/ui/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

46
gm-dash/ui/README.md Normal file
View File

@ -0,0 +1,46 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

18044
gm-dash/ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
gm-dash/ui/package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "ui",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"@types/jest": "^27.5.2",
"@types/node": "^16.18.105",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"classnames": "^2.5.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.1",
"react-scripts": "5.0.1",
"typescript": "^4.9.5",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,5 @@
.layout {
display: flex;
justify-content: space-between;
width: 100%;
}

View File

@ -0,0 +1,9 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import Dashboard from './Dashboard';
test('renders learn react link', () => {
render(<Dashboard />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@ -0,0 +1,77 @@
import './Dashboard.css';
import Card from './components/Card/Card';
import Launcher from './components/Launcher/Launcher';
import Launchpad from './components/Launchpad/Launchpad';
const LightThemes = () => <Card name="Light Themes">
<Launchpad
exclusive={true}
options={[
{ title: "Dark reds" },
{ title: "Watery" },
{ title: "Sunset" },
{ title: "Darkness" },
]}
/>
</Card>
const LightSetup = () => <div> </div>
interface LightProps {
name: string,
}
const Light = ({ name }: LightProps) => <div> <p> {name} </p> </div>
const Tracks = () => <Card name="Tracks">
<Launchpad
exclusive={false}
options={[
{ title: "City BGM" },
{ title: "Chat on the streets" },
{ title: "Abandoned structure" },
{ title: "Water dripping" },
]}
/>
</Card>
interface TrackProps {
name: string,
}
const Track = ({ name }: TrackProps) => <Launcher title={name} />
const Presets = () => <Card name="Presets">
<Launchpad
exclusive={true}
options={[
{ title: "Gilcrest Falls day" },
{ title: "Gilcrest Falls night" },
{ title: "Empty colony" },
{ title: "Surk colony" },
]}
/>
</Card>
interface PresetProps {
name: string
}
// const Scene = ({ name }: PresetProps) => <Launcher title={name} activated={false} />
const SceneEditor = () => <div> </div>
const Dashboard = () => {
return (
<div className="app">
<div className="layout">
<Presets />
<div>
<LightThemes />
<Tracks />
</div>
</div>
</div>
);
}
export default Dashboard;

78
gm-dash/ui/src/Design.css Normal file
View File

@ -0,0 +1,78 @@
.palette {
display: flex;
}
.palette div {
width: 50px;
height: 50px;
border: 1px solid black;
border-radius: 5px;
margin: 1em;
padding: 0;
}
.palette-purple div.item-1 {
background-color: var(--purple-1);
}
.palette-purple div.item-2 {
background-color: var(--purple-2);
}
.palette-purple div.item-3 {
background-color: var(--purple-3);
}
.palette-purple div.item-4 {
background-color: var(--purple-4);
}
.palette-purple div.item-5 {
background-color: var(--purple-5);
}
.palette-blue div.item-1 {
background-color: var(--blue-1);
}
.palette-blue div.item-2 {
background-color: var(--blue-2);
}
.palette-blue div.item-3 {
background-color: var(--blue-3);
}
.palette-blue div.item-4 {
background-color: var(--blue-4);
}
.palette-blue div.item-5 {
background-color: var(--blue-5);
}
.palette-grey div.item-1 {
background-color: var(--grey-1);
}
.palette-grey div.item-2 {
background-color: var(--grey-2);
}
.palette-grey div.item-3 {
background-color: var(--grey-3);
}
.palette-grey div.item-4 {
background-color: var(--grey-4);
}
.palette-grey div.item-5 {
background-color: var(--grey-5);
}
.design_horizontal {
display: flex;
flex-flow: row;
}

57
gm-dash/ui/src/Design.tsx Normal file
View File

@ -0,0 +1,57 @@
import './Design.css'
import Launchpad from './components/Launchpad/Launchpad'
const PaletteGrey = () => <div className="palette palette-grey">
<div className="item-1" />
<div className="item-2" />
<div className="item-3" />
<div className="item-4" />
<div className="item-5" />
</div>
const PalettePurple = () => <div className="palette palette-purple">
<div className="item-1" />
<div className="item-2" />
<div className="item-3" />
<div className="item-4" />
<div className="item-5" />
</div>
const PaletteBlue = () => <div className="palette palette-blue">
<div className="item-1" />
<div className="item-2" />
<div className="item-3" />
<div className="item-4" />
<div className="item-5" />
</div>
const Launchpads = () => <div className="design_horizontal">
<Launchpad
exclusive={true}
options={[
{ title: "Grey" },
{ title: "Purple" },
{ title: "Blue" },
]}
/>
<Launchpad
exclusive={false}
flow={"vertical"}
options={[
{ title: "Grey" },
{ title: "Purple" },
{ title: "Blue" },
]}
/>
</div>
const Design = () => <div>
<PaletteGrey />
<PalettePurple />
<PaletteBlue />
<Launchpads />
</div>
export default Design;

View File

@ -0,0 +1,15 @@
.card {
border: 1px solid black;
border-radius: 5px;
margin: var(--spacer-l);
box-shadow: 4px 4px 4px 0px var(--shadow-1),
8px 8px 8px 0px var(--shadow-2);
}
.card__title {
color: var(--title-color);
}
.card__body {
}

View File

@ -0,0 +1,17 @@
import { PropsWithChildren } from 'react';
import './Card.css';
interface CardProps {
name: string,
}
const Card = ({ name, children }: PropsWithChildren<CardProps>) => (
<div className="card">
<h1 className="card__title"> {name} </h1>
<div className="card__body">
{children}
</div>
</div>
)
export default Card;

View File

@ -0,0 +1,11 @@
.activator {
border: 1px solid black;
border-radius: 5px;
margin: var(--spacer-m);
padding: var(--spacer-s);
box-shadow: var(--shadow-deep);
}
.activator_enabled {
box-shadow: var(--activator-ring);
}

View File

@ -0,0 +1,20 @@
import './Launcher.css';
import React from 'react';
export interface LauncherProps {
title: string,
icon?: string,
activated?: boolean,
onSelected?: (key: string) => void,
}
const Launcher = ({ title, activated = false, onSelected = (key) => {} }: LauncherProps) => {
const classnames = activated ? "activator activator_enabled" : "activator";
console.log("classnames ", activated, classnames);
return (
<div className={classnames} onClick={() => onSelected(title)}>
<p> {title} </p>
</div>)
}
export default Launcher;

View File

@ -0,0 +1,18 @@
.launchpad {
display: flex;
border: var(--border);
border-radius: var(--border-radius);
box-shadow: var(--shadow-depression);
margin: var(--spacer-l);
}
.launchpad_horizontal-flow {
display: flex;
flex-direction: row;
}
.launchpad_vertical-flow {
display: flex;
flex-direction: column;
}

View File

@ -0,0 +1,49 @@
import React from 'react';
import './Launchpad.css';
import Launcher, { LauncherProps } from '../Launcher/Launcher';
import classnames from 'classnames';
export interface Selectable {
onSelected?: (key: string) => void;
}
export type Flow = "horizontal" | "vertical";
interface LaunchpadProps {
exclusive: boolean;
flow?: Flow;
options: Array<LauncherProps>;
}
const exclusiveSelect = (state: { [key: string]: boolean }, targetId: string) => {
console.log("running exclusiveSelect on ", targetId);
return { [targetId]: true };
}
const multiSelect = (state: { [key: string]: boolean }, targetId: string) => {
if (state[targetId]) {
return { ...state, [targetId]: false };
} else {
return { ...state, [targetId]: true };
}
}
const Launchpad = ({ flow = "horizontal", options, exclusive }: LaunchpadProps) => {
const [selected, dispatch] = React.useReducer(exclusive ? exclusiveSelect : multiSelect, {});
let classOptions = [ "launchpad" ];
if (flow === "horizontal") {
classOptions.push("launchpad_horizontal-flow");
} else {
classOptions.push("launchpad_vertical-flow");
}
let tiedOptions = options.map(option =>
<Launcher key={option.title} title={option.title} onSelected={(key: string) => dispatch(key)} activated={selected[option.title]} />
);
return (<div className={classnames(classOptions)}> {tiedOptions} </div>)
}
export default Launchpad;

50
gm-dash/ui/src/index.css Normal file
View File

@ -0,0 +1,50 @@
:root {
--purple-1: hsl(265, 50%, 25%);
--purple-2: hsl(265, 60%, 35%);
--purple-3: hsl(265, 70%, 45%);
--purple-4: hsl(265, 80%, 55%);
--purple-5: hsl(265, 90%, 60%);
--blue-1: hsl(210, 50%, 25%);
--blue-2: hsl(210, 60%, 35%);
--blue-3: hsl(210, 70%, 45%);
--blue-4: hsl(210, 80%, 55%);
--blue-5: hsl(210, 90%, 65%);
--grey-1: hsl(210, 0%, 25%);
--grey-2: hsl(210, 0%, 40%);
--grey-3: hsl(210, 0%, 55%);
--grey-4: hsl(210, 0%, 70%);
--grey-5: hsl(210, 0%, 85%);
--title-color: var(--grey-1);
--border: 1px solid var(--purple-1);
--activator-ring: 0px 0px 8px 4px var(--blue-4);
--shadow-depression: inset 1px 1px 2px 0px var(--purple-1);
--shadow-shallow: 1px 1px 2px 0px var(--purple-1);
--shadow-deep: 2px 2px 4px 0px var(--purple-1),
4px 4px 8px 0px var(--purple-2);
--border-radius: 8px;
--spacer-xs: 2px;
--spacer-s: 4px;
--spacer-m: 8px;
--spacer-l: 12px;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--grey-5);
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

31
gm-dash/ui/src/index.tsx Normal file
View File

@ -0,0 +1,31 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import Dashboard from './Dashboard';
import Design from './Design';
import reportWebVitals from './reportWebVitals';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
const router = createBrowserRouter([
{
path: "/",
element: <Dashboard />,
},
{
path: "/design",
element: <Design />,
},
]);
root.render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

1
gm-dash/ui/src/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

1
gm-dash/ui/src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@ -0,0 +1,15 @@
import { ReportHandler } from 'web-vitals';
const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

26
gm-dash/ui/tsconfig.json Normal file
View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": [
"src"
]
}

View File

@ -14,6 +14,7 @@ sgf = { path = "../../sgf" }
grid = { version = "0.9" }
serde_json = { version = "1" }
serde = { version = "1", features = [ "derive" ] }
nary_tree = { version = "0.4" }
thiserror = { version = "1" }
uuid = { version = "0.8", features = ["v4", "serde"] }

View File

@ -81,7 +81,7 @@ impl From<HotseatPlayerRequest> for Player {
}
*/
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug)]
pub enum CoreResponse {
Library(library::LibraryResponse),
Settings(settings::SettingsResponse),
@ -115,8 +115,6 @@ pub struct Core {
impl Core {
pub fn new(config: Config) -> Self {
println!("config: {:?}", config);
let library = match config.get::<LibraryPath>() {
Some(ref path) if path.to_path_buf().exists() => {
Some(Database::open_path(path.to_path_buf()).unwrap())

View File

@ -42,7 +42,8 @@ impl Database {
.unwrap();
match parse_sgf(&buffer) {
Ok(sgfs) => {
let mut sgfs = sgfs.into_iter().flatten().collect::<Vec<sgf::GameRecord>>();
let mut sgfs =
sgfs.into_iter().flatten().collect::<Vec<sgf::GameRecord>>();
games.append(&mut sgfs);
}
Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err),

View File

@ -213,7 +213,7 @@ impl Goban {
///
/// assert_eq!(goban.stone(&Coordinate{ row: 3, column: 3 }), Some(Color::Black));
/// assert_eq!(goban.stone(&Coordinate{ row: 15, column: 15 }), Some(Color::White));
/// assert_eq!(goban.stone(&Coordinate{ row: 3, column: 15 }), Some(Color::Black));
/// assert_eq!(goban.stone(&Coordinate{ row: 15, column: 3 }), Some(Color::Black));
/// ```
pub fn apply_moves<'a>(
self,
@ -611,7 +611,6 @@ mod test {
),
];
println!("{}", board);
for (board, coordinate, group, liberties) in test_cases {
assert_eq!(board.group(&coordinate), group.as_ref());
assert_eq!(

View File

@ -29,5 +29,9 @@ pub mod library;
pub mod settings;
mod types;
pub use types::{BoardError, Color, Config, ConfigOption, LibraryPath, Player, Rank, Size, Tree};
pub use types::{
BoardError, Color, Config, ConfigOption, DepthTree, LibraryPath, Player, Rank, Size,
};
mod view_models;
pub use view_models::GameReviewViewModel;

View File

@ -14,18 +14,18 @@ General Public License for more details.
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::{Core};
use crate::Core;
use serde::{Deserialize, Serialize};
use sgf::GameRecord;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum LibraryRequest {
ListGames
ListGames,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug)]
pub enum LibraryResponse {
Games(Vec<GameRecord>)
Games(Vec<GameRecord>),
}
async fn handle_list_games(model: &Core) -> LibraryResponse {
@ -39,10 +39,8 @@ async fn handle_list_games(model: &Core) -> LibraryResponse {
}
}
pub async fn handle(model: &Core, request: LibraryRequest) -> LibraryResponse {
match request {
LibraryRequest::ListGames => handle_list_games(model).await,
}
}

View File

@ -1,11 +1,13 @@
use crate::goban::{Coordinate, Goban};
use config::define_config;
use config_derive::ConfigOption;
use nary_tree::NodeRef;
use serde::{Deserialize, Serialize};
use sgf::GameNode;
use std::{cell::RefCell, collections::VecDeque, fmt, path::PathBuf, time::Duration};
use sgf::GameTree;
use std::{
collections::{HashMap, VecDeque}, fmt, ops::Deref, path::PathBuf, time::Duration
};
use thiserror::Error;
use uuid::Uuid;
define_config! {
LibraryPath(LibraryPath),
@ -229,6 +231,7 @@ impl GameState {
}
}
/*
// To properly generate a tree, I need to know how deep to go. Then I can backtrace. Each node
// needs to have a depth. Given a tree, the depth of the node is just the distance from the root.
// This seems obvious, but I had to write it to discover how important that fact was.
@ -238,33 +241,88 @@ impl GameState {
pub struct Tree<T> {
nodes: Vec<Node<T>>,
}
*/
#[derive(Debug)]
pub struct Node<T> {
pub id: usize,
node: T,
parent: Option<usize>,
depth: usize,
width: RefCell<Option<usize>>,
children: Vec<usize>,
// https://llimllib.github.io/pymag-trees/
// I want to take advantage of the Wetherell Shannon algorithm, but I want some variations. In
// their diagram, they got a tree that looks like this.
//
// O
// |\
// O O
// |\ \ \
// O O O O
// |\ |\
// O O O O
//
// In the same circumstance, what I want is this:
//
// O--
// | \
// O O
// |\ |\
// O O O O
// |\
// O O
//
// In order to keep things from being overly smooshed, I want to ensure that if a branch overlaps
// with another branch, there is some extra drawing space. This might actually be similar to adding
// the principal that "A parent should be centered over its children".
//
// So, given a tree, I need to know how many children exist at each level. Then I build parents
// atop the children. At level 3, I have four children, and that happens to be the maximum width of
// the graph.
//
// A bottom-up traversal:
// - Figure out the number of nodes at each depth
pub struct DepthTree(nary_tree::Tree<SizeNode>);
impl Deref for DepthTree {
type Target = nary_tree::Tree<SizeNode>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> Tree<T> {
fn new(root: T) -> Self {
Tree {
nodes: vec![Node {
id: 0,
node: root,
parent: None,
depth: 0,
width: RefCell::new(None),
children: vec![],
}],
}
impl Default for DepthTree {
fn default() -> Self {
Self(nary_tree::Tree::new())
}
}
#[derive(Debug)]
pub struct SizeNode {
/// Use this to map back to the node in the original game tree. This way we know how to
/// correspond from a node in the review tree back to there.
pub game_node_id: nary_tree::NodeId,
/// How deep into the tree is this node?
depth: usize,
/// How far from the leftmost margin is this node?
width: usize,
}
impl SizeNode {
pub fn position(&self) -> (usize, usize) {
(self.depth, self.width)
}
}
impl DepthTree {
/*
pub fn node(&self, idx: usize) -> &T {
&self.nodes[idx].content
}
pub fn node(&self, idx: usize) -> &T {
&self.nodes[idx].node
pub fn parent(&self, node: &Node<T>) -> Option<&Node<T>> {
if let Some(parent_idx) = node.parent {
self.nodes.get(parent_idx)
} else {
None
}
}
// Add a node to the parent specified by parent_idx. Return the new index. This cannot be used
@ -275,7 +333,7 @@ impl<T> Tree<T> {
self.nodes.push(Node {
id: next_idx,
node,
content: node,
parent: Some(parent_idx),
depth: parent.depth + 1,
width: RefCell::new(None),
@ -286,12 +344,20 @@ impl<T> Tree<T> {
parent.children.push(next_idx);
next_idx
}
*/
pub fn max_depth(&self) -> usize {
self.nodes.iter().fold(
0,
|max, node| if node.depth > max { node.depth } else { max },
)
self.0
.root()
.unwrap()
.traverse_pre_order()
.fold(0, |max, node| {
if node.data().depth > max {
node.data().depth
} else {
max
}
})
}
// Since I know the width of a node, now I want to figure out its placement in the larger
@ -309,7 +375,9 @@ impl<T> Tree<T> {
// amounts to the position of the parent node.
//
// When drawing nodes, I don't know how to persist the level of indent.
pub fn position(&self, idx: usize) -> (usize, usize) {
// unimplemented!()
/*
let node = &self.nodes[idx];
match node.parent {
Some(parent_idx) => {
@ -320,15 +388,15 @@ impl<T> Tree<T> {
.iter()
.take_while(|n| **n != node.id)
.fold(0, |acc, n| acc + self.width(*n));
println!("[{}] sibling width {}", idx, sibling_width);
(node.depth, parent_column + sibling_width)
}
// Root nodes won't have a parent, so just put them in the first column
None => (0, 0),
}
}
*/
/*
// Given a node, do a postorder traversal to figure out the width of the node based on all of
// its children. This is equivalent to the widest of all of its children at all depths.
//
@ -340,7 +408,6 @@ impl<T> Tree<T> {
// My algorithm right now is likely to generate unnecessarily wide trees in a complex game
// review.
fn width(&self, id: usize) -> usize {
println!("[{}] calculating width", id);
let node = &self.nodes[id];
if let Some(width) = *node.width.borrow() {
return width;
@ -351,22 +418,107 @@ impl<T> Tree<T> {
.iter()
.fold(0, |acc, child| acc + self.width(*child));
let width = if width == 0 { 1 } else { width };
println!("[{}] width: {}", id, width);
*node.width.borrow_mut() = Some(width);
width
}
*/
pub fn bfs_iter<'a>(&'a self) -> BFSIter<T> {
pub fn bfs_iter(&self) -> BFSIter<'_, SizeNode> {
let mut queue = VecDeque::new();
queue.push_back(&self.nodes[0]);
BFSIter { tree: self, queue }
queue.push_back(self.0.root().unwrap());
BFSIter { queue }
}
}
impl<'a> From<&'a GameTree> for DepthTree {
fn from(tree: &'a GameTree) -> Self {
// Like in the conversion from SGF to GameTree, I need to traverse the entire tree one node
// at a time, keeping track of node ids as we go. I'm going to go with a depth-first
// traversal. When generating each node, I think I want to generate all of the details of
// the node as we go.
let source_root_node = tree.root();
match source_root_node {
Some(source_root_node) => {
// Do the real work
// The id_map indexes from the source tree to the destination tree. Reverse
// indexing is accomplished by looking at the node_id in a node in the destination
// tree.
let mut id_map: HashMap<nary_tree::NodeId, nary_tree::NodeId> = HashMap::new();
let mut tree = nary_tree::Tree::new();
let mut iter = source_root_node.traverse_pre_order();
let _ = iter.next().unwrap(); // we already know that the first element to be
// returned is the root node, and that the root node
// already exists. Otherwise we wouldn't even be in
// this branch.
let dest_root_id = tree.set_root(SizeNode {
game_node_id: source_root_node.node_id(),
depth: 0,
width: 0,
});
id_map.insert(source_root_node.node_id(), dest_root_id);
for source_node in iter {
let dest_parent_id = id_map
.get(&source_node.parent().unwrap().node_id())
.unwrap();
let mut dest_parent = tree.get_mut(*dest_parent_id).unwrap();
let new_depth_node = SizeNode {
game_node_id: source_node.node_id(),
depth: 1 + dest_parent.data().depth,
width: dest_parent.data().width,
};
let new_node_id = dest_parent.append(new_depth_node).node_id();
match tree
.get(new_node_id)
.unwrap()
.prev_sibling()
.map(|node| node.data().width)
{
None => {}
Some(previous_width) => {
let mut new_node = tree.get_mut(new_node_id).unwrap();
new_node.data().width = previous_width + 1;
}
}
/*
let new_node = tree.get_mut(*dest_parent_id).unwrap().append(new_depth_node);
let previous_node = new_node.prev_sibling();
match previous_node {
None => {}
}
*/
/*
match dest_noderef.prev_sibling() {
None => {}
Some(mut node) => { dest_noderef.data().width = node.data().width + 1 }
}
*/
id_map.insert(source_node.node_id(), new_node_id);
}
Self(tree)
}
None => Self::default(),
}
}
}
/*
impl<'a> From<&'a GameNode> for Tree<Uuid> {
fn from(root: &'a GameNode) -> Self {
fn add_subtree<'a>(tree: &mut Tree<Uuid>, parent_idx: usize, node: &'a GameNode) {
fn add_subtree(tree: &mut Tree<Uuid>, parent_idx: usize, node: &GameNode) {
let idx = tree.add_node(parent_idx, node.id());
let children = match node {
@ -393,22 +545,21 @@ impl<'a> From<&'a GameNode> for Tree<Uuid> {
tree
}
}
*/
pub struct BFSIter<'a, T> {
tree: &'a Tree<T>,
queue: VecDeque<&'a Node<T>>,
queue: VecDeque<nary_tree::NodeRef<'a, T>>,
}
impl<'a, T> Iterator for BFSIter<'a, T> {
type Item = &'a Node<T>;
type Item = NodeRef<'a, T>;
fn next(&mut self) -> Option<Self::Item> {
let retval = self.queue.pop_front();
if let Some(ref retval) = retval {
retval
.children
.iter()
.for_each(|idx| self.queue.push_back(&self.tree.nodes[*idx]));
.children()
.for_each(|noderef| self.queue.push_back(noderef));
}
retval
}
@ -417,8 +568,8 @@ impl<'a, T> Iterator for BFSIter<'a, T> {
#[cfg(test)]
mod test {
use super::*;
use cool_asserts::assert_matches;
use sgf::{Move, MoveNode};
// use sgf::{GameRecord, GameTree, GameType, Move, MoveNode};
use sgf::{GameNode, GameTree, Move, MoveNode};
#[test]
fn current_player_changes_after_move() {
@ -477,116 +628,131 @@ mod test {
// B G H
// C I
// D E F
fn branching_tree() -> GameTree {
let mut game_tree = GameTree::default();
let node_a = game_tree.set_root(GameNode::MoveNode(MoveNode::new(
sgf::Color::Black,
Move::Move("dp".to_owned()),
)));
let node_b = game_tree
.get_mut(node_a)
.unwrap()
.append(GameNode::MoveNode(MoveNode::new(
sgf::Color::Black,
Move::Move("dp".to_owned()),
)))
.node_id();
let node_c = game_tree
.get_mut(node_b)
.unwrap()
.append(GameNode::MoveNode(MoveNode::new(
sgf::Color::Black,
Move::Move("dp".to_owned()),
)))
.node_id();
let _node_d = game_tree
.get_mut(node_c)
.unwrap()
.append(GameNode::MoveNode(MoveNode::new(
sgf::Color::Black,
Move::Move("dp".to_owned()),
)))
.node_id();
let _node_e = game_tree
.get_mut(node_c)
.unwrap()
.append(GameNode::MoveNode(MoveNode::new(
sgf::Color::Black,
Move::Move("dp".to_owned()),
)))
.node_id();
let _node_f = game_tree
.get_mut(node_c)
.unwrap()
.append(GameNode::MoveNode(MoveNode::new(
sgf::Color::Black,
Move::Move("dp".to_owned()),
)))
.node_id();
let _node_g = game_tree
.get_mut(node_a)
.unwrap()
.append(GameNode::MoveNode(MoveNode::new(
sgf::Color::Black,
Move::Move("dp".to_owned()),
)))
.node_id();
let node_h = game_tree
.get_mut(node_a)
.unwrap()
.append(GameNode::MoveNode(MoveNode::new(
sgf::Color::Black,
Move::Move("dp".to_owned()),
)))
.node_id();
let _ = game_tree
.get_mut(node_h)
.unwrap()
.append(GameNode::MoveNode(MoveNode::new(
sgf::Color::Black,
Move::Move("dp".to_owned()),
)))
.node_id();
game_tree
}
#[test]
fn it_can_calculate_depth_from_game_tree() {
let mut node_a = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_b = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_c = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_d = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_e = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_f = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_g = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_h = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_i = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
node_c.children.push(GameNode::MoveNode(node_d));
node_c.children.push(GameNode::MoveNode(node_e));
node_c.children.push(GameNode::MoveNode(node_f));
node_b.children.push(GameNode::MoveNode(node_c));
node_h.children.push(GameNode::MoveNode(node_i));
node_a.children.push(GameNode::MoveNode(node_b));
node_a.children.push(GameNode::MoveNode(node_g));
node_a.children.push(GameNode::MoveNode(node_h));
let game_tree = GameNode::MoveNode(node_a);
let tree = Tree::from(&game_tree);
let game_tree = branching_tree();
let tree = DepthTree::from(&game_tree);
assert_eq!(
game_tree.root().unwrap().traverse_pre_order().count(),
tree.0.root().unwrap().traverse_pre_order().count()
);
assert_eq!(tree.max_depth(), 3);
}
// A
// B G H
// C I
// D E F
#[test]
fn it_calculates_horizontal_position_of_nodes() {
let mut node_a = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_b = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_c = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_d = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_e = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_f = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_g = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_h = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_i = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let game_tree = branching_tree();
let tree = DepthTree::from(&game_tree);
node_c.children.push(GameNode::MoveNode(node_d));
node_c.children.push(GameNode::MoveNode(node_e));
node_c.children.push(GameNode::MoveNode(node_f));
let node_a = tree.root().unwrap();
assert_eq!(node_a.data().position(), (0, 0));
node_b.children.push(GameNode::MoveNode(node_c));
let node_b = node_a.first_child().unwrap();
assert_eq!(node_b.data().position(), (1, 0));
let node_g = node_b.next_sibling().unwrap();
assert_eq!(node_g.data().position(), (1, 1));
let node_h = node_g.next_sibling().unwrap();
assert_eq!(node_h.data().position(), (1, 2));
node_h.children.push(GameNode::MoveNode(node_i));
let node_c = node_b.first_child().unwrap();
assert_eq!(node_c.data().position(), (2, 0));
node_a.children.push(GameNode::MoveNode(node_b));
node_a.children.push(GameNode::MoveNode(node_g));
node_a.children.push(GameNode::MoveNode(node_h));
let node_d = node_c.first_child().unwrap();
assert_eq!(node_d.data().position(), (3, 0));
let game_tree = GameNode::MoveNode(node_a);
let node_i = node_h.first_child().unwrap();
assert_eq!(node_i.data().position(), (2, 2));
let tree = Tree::from(&game_tree);
assert_eq!(tree.position(2), (2, 0));
assert_eq!(tree.position(1), (1, 0));
assert_eq!(tree.position(0), (0, 0));
assert_eq!(tree.position(4), (3, 1));
assert_eq!(tree.position(5), (3, 2));
assert_eq!(tree.position(6), (1, 3));
assert_eq!(tree.position(7), (1, 4));
}
#[test]
fn breadth_first_iter() {
let mut node_a = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_b = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_c = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_d = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_e = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_f = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_g = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let mut node_h = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
let node_i = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
node_c.children.push(GameNode::MoveNode(node_d.clone()));
node_c.children.push(GameNode::MoveNode(node_e.clone()));
node_c.children.push(GameNode::MoveNode(node_f.clone()));
node_b.children.push(GameNode::MoveNode(node_c.clone()));
node_h.children.push(GameNode::MoveNode(node_i.clone()));
node_a.children.push(GameNode::MoveNode(node_b.clone()));
node_a.children.push(GameNode::MoveNode(node_g.clone()));
node_a.children.push(GameNode::MoveNode(node_h.clone()));
let game_tree = GameNode::MoveNode(node_a.clone());
let tree = Tree::from(&game_tree);
let mut iter = tree.bfs_iter();
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_a.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_b.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_g.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_h.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_c.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_i.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_d.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_e.id));
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_f.id));
/*
assert_eq!(tree.position(test_tree.node_c), (2, 0));
assert_eq!(tree.position(test_tree.node_b), (1, 0));
assert_eq!(tree.position(test_tree.node_a), (0, 0));
assert_eq!(tree.position(test_tree.node_d), (3, 1));
assert_eq!(tree.position(test_tree.node_e), (3, 2));
assert_eq!(tree.position(test_tree.node_f), (1, 3));
assert_eq!(tree.position(test_tree.node_g), (1, 4));
*/
}
}

View File

@ -0,0 +1,281 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of On the Grid.
On the Grid is free software: you can redistribute it and/or modify it under the terms of
the GNU General Public License as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
On the Grid is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
*/
// Currenty my game review is able to show the current game and its tree. Now, I want to start
// tracking where in the tree that I am. This should be a combination of the abstract Tree and the
// gameTree. Chances are, if I just keep track of where I am in the abstract tree, I can find the
// relevant node in the game tree and then reproduce the line to get to that node.
//
// Moving through the game review tree shouldn't require a full invocatian. This object, and most
// other view models, should be exported to the UI.
use crate::{types::SizeNode, DepthTree, Goban};
use nary_tree::{NodeId, NodeRef};
use sgf::{GameRecord, Player};
use std::sync::{Arc, RwLock};
struct GameReviewViewModelPrivate {
// This is the ID of the current position in the game. The ID is specific to the GameRecord,
// not the ReviewTree.
current_position: Option<NodeId>,
game: GameRecord,
review_tree: DepthTree,
}
#[derive(Clone)]
pub struct GameReviewViewModel {
inner: Arc<RwLock<GameReviewViewModelPrivate>>,
}
impl GameReviewViewModel {
pub fn new(game: GameRecord) -> Self {
let (review_tree, current_position) = if !game.trees.is_empty() {
let review_tree = DepthTree::from(&game.trees[0]);
let current_position = game.mainline().unwrap().last().map(|nr| nr.node_id());
(review_tree, current_position)
} else {
(DepthTree::default(), None)
};
Self {
inner: Arc::new(RwLock::new(GameReviewViewModelPrivate {
current_position,
game,
review_tree,
})),
}
}
pub fn black_player(&self) -> Player {
self.inner.read().unwrap().game.black_player.clone()
}
pub fn white_player(&self) -> Player {
self.inner.read().unwrap().game.white_player.clone()
}
pub fn game_view(&self) -> Goban {
let inner = self.inner.read().unwrap();
let mut path: Vec<NodeId> = vec![];
let mut current_id = inner.current_position;
while current_id.is_some() {
let current = current_id.unwrap();
path.push(current);
current_id = inner.game.trees[0]
.get(current)
.unwrap()
.parent()
.map(|parent| parent.node_id());
}
path.reverse();
Goban::default()
.apply_moves(
path.into_iter()
.map(|node_id| inner.game.trees[0].get(node_id).unwrap().data()),
)
.unwrap()
/*
if let Some(start) = inner.current_position {
let mut current_id = start;
let mut path = vec![current_id.clone()];
while let
/*
let mut current_node = inner.game.trees[0].get(current_position).unwrap();
let mut path = vec![current_node.data()];
while let Some(parent) = current_node.parent() {
path.push(parent.data());
current_node = parent;
}
*/
path.reverse();
Goban::default().apply_moves(path).unwrap()
} else {
Goban::default()
}
*/
}
pub fn map_tree<F>(&self, f: F)
where
F: Fn(NodeRef<'_, SizeNode>, Option<NodeId>),
{
let inner = self.inner.read().unwrap();
for node in inner.review_tree.bfs_iter() {
f(node, inner.current_position);
}
}
pub fn tree_max_depth(&self) -> usize {
self.inner.read().unwrap().review_tree.max_depth()
}
// When moving forward on the tree, I grab the first child by default. I can then just advance
// the board state by applying the child.
pub fn next_move(&self) {
let mut inner = self.inner.write().unwrap();
let current_position = inner.current_position.clone();
match current_position {
Some(current_position) => {
let current_id = current_position.clone();
let node = inner.game.trees[0].get(current_id).unwrap();
if let Some(next_id) = node.first_child().map(|child| child.node_id()) {
inner.current_position = Some(next_id);
}
}
None => {
inner.current_position = inner.game.trees[0].root().map(|node| node.node_id());
}
}
}
// When moving backwards, I jump up to the parent. I'll then rebuild the board state from the
// root.
pub fn previous_move(&mut self) {
let mut inner = self.inner.write().unwrap();
if let Some(current_position) = inner.current_position {
let current_node = inner.game.trees[0]
.get(current_position)
.expect("current_position should always correspond to a node in the tree");
if let Some(parent_node) = current_node.parent() {
inner.current_position = Some(parent_node.node_id());
}
}
}
pub fn next_variant(&self) {
println!("move to the next variant amongst the options available");
}
pub fn previous_variant(&self) {
println!("move to the previous variant amongst the options available");
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{Color, Coordinate};
use std::path::Path;
fn with_game_record<F>(test: F)
where
F: FnOnce(GameReviewViewModel),
{
let records = sgf::parse_sgf_file(&Path::new("../../sgf/test_data/branch_test.sgf"))
.expect("to successfully load the test file");
let record = records[0]
.as_ref()
.expect("to have successfully loaded the test record");
let view_model = GameReviewViewModel::new(record.clone());
test(view_model);
}
#[test]
fn it_generates_a_mainline_board() {
with_game_record(|view| {
let goban = view.game_view();
for row in 0..18 {
for column in 0..18 {
if row == 3 && column == 3 {
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
} else if row == 15 && column == 3 {
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
} else if row == 3 && column == 15 {
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
} else if row == 15 && column == 14 {
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
} else {
assert_eq!(
goban.stone(&Coordinate { row, column }),
None,
"{} {}",
row,
column
);
}
}
}
});
}
#[test]
fn it_moves_to_the_previous_mainline_move() {
with_game_record(|mut view| {
view.previous_move();
let goban = view.game_view();
for row in 0..18 {
for column in 0..18 {
if row == 3 && column == 3 {
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
} else if row == 3 && column == 15 {
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
} else if row == 15 && column == 14 {
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
} else {
assert_eq!(
goban.stone(&Coordinate { row, column }),
None,
"{} {}",
row,
column
);
}
}
}
});
}
#[test]
fn it_moves_to_the_next_node() {
with_game_record(|mut view| {
view.previous_move();
view.next_move();
let goban = view.game_view();
for row in 0..18 {
for column in 0..18 {
if row == 3 && column == 3 {
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
} else if row == 15 && column == 3 {
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
} else if row == 3 && column == 15 {
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
} else if row == 15 && column == 14 {
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
} else {
assert_eq!(
goban.stone(&Coordinate { row, column }),
None,
"{} {}",
row,
column
);
}
}
}
});
}
}

View File

@ -0,0 +1,20 @@
/*
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
This file is part of On the Grid.
On the Grid is free software: you can redistribute it and/or modify it under the terms of
the GNU General Public License as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version.
On the Grid is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
*/
mod game_review;
pub use game_review::GameReviewViewModel;

View File

@ -3,5 +3,7 @@
<gresource prefix="/com/luminescent-dreams/otg-gtk/">
<file>wood_texture.jpg</file>
<file>style.css</file>
<file>black_stone.png</file>
<file>white_stone.png</file>
</gresource>
</gresources>

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 350 KiB

After

Width:  |  Height:  |  Size: 698 KiB

View File

@ -14,12 +14,14 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::CoreApi;
use crate::{CoreApi, ResourceManager};
use adw::prelude::*;
use glib::Propagation;
use gtk::{gdk::Key, EventControllerKey};
use otg_core::{
settings::{SettingsRequest, SettingsResponse},
CoreRequest, CoreResponse,
CoreRequest, CoreResponse, GameReviewViewModel,
};
use sgf::GameRecord;
use std::sync::{Arc, RwLock};
@ -58,10 +60,12 @@ pub struct AppWindow {
// Not liking this, but I have to keep track of the settings view model separately from
// anything else. I'll have to look into this later.
settings_view_model: Arc<RwLock<Option<SettingsView>>>,
resources: ResourceManager,
}
impl AppWindow {
pub fn new(app: &adw::Application, core: CoreApi) -> Self {
pub fn new(app: &adw::Application, core: CoreApi, resources: ResourceManager) -> Self {
let window = Self::setup_window(app);
let overlay = Self::setup_overlay();
let stack = adw::NavigationView::new();
@ -77,6 +81,7 @@ impl AppWindow {
overlay,
core,
settings_view_model: Default::default(),
resources,
};
let home = s.setup_home();
@ -88,13 +93,32 @@ impl AppWindow {
pub fn open_game_review(&self, game_record: GameRecord) {
let header = adw::HeaderBar::new();
let game_review = GameReview::new(self.core.clone(), game_record);
let game_review = GameReview::new(
GameReviewViewModel::new(game_record),
self.resources.clone(),
);
let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.build();
layout.append(&header);
layout.append(&game_review);
layout.append(&game_review.widget());
// This controller ensures that navigational keypresses get sent to the game review so that
// they're not changing the cursor focus in the app.
let keypress_controller = EventControllerKey::new();
keypress_controller.connect_key_pressed({
move |s, key, _, _| {
println!("layout keypress: {}", key);
if s.forward(&game_review.widget()) {
Propagation::Stop
} else {
Propagation::Proceed
}
}
});
layout.add_controller(keypress_controller);
let page = adw::NavigationPage::builder()
.can_pop(true)

View File

@ -35,14 +35,10 @@ You should have received a copy of the GNU General Public License along with On
// Now, we know what kind of object we have for the current board representation. Let's make use of
// that.
use crate::perftrace;
use crate::{perftrace, Resource, ResourceManager};
use glib::Object;
use gtk::{
prelude::*,
subclass::prelude::*,
};
use gtk::{gdk_pixbuf::Pixbuf, prelude::*, subclass::prelude::*};
use otg_core::{Color, Coordinate};
use std::{cell::RefCell, rc::Rc};
@ -54,6 +50,7 @@ const MARGIN: i32 = 20;
#[derive(Default)]
pub struct GobanPrivate {
board_state: Rc<RefCell<otg_core::Goban>>,
resource_manager: Rc<RefCell<Option<ResourceManager>>>,
}
impl GobanPrivate {}
@ -86,10 +83,11 @@ glib::wrapper! {
}
impl Goban {
pub fn new(board_state: otg_core::Goban) -> Self {
pub fn new(board_state: otg_core::Goban, resources: ResourceManager) -> Self {
let s: Self = Object::builder().build();
*s.imp().board_state.borrow_mut() = board_state;
*s.imp().resource_manager.borrow_mut() = Some(resources);
s.set_width_request(WIDTH);
s.set_height_request(HEIGHT);
@ -103,43 +101,40 @@ impl Goban {
s
}
fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) {
println!("{} x {}", width, height);
/*
let wood_texture = resources_lookup_data(
"/com/luminescent-dreams/otg-gtk/wood_texture.jpg",
gio::ResourceLookupFlags::NONE,
)
.unwrap();
pub fn set_board_state(&mut self, board_state: otg_core::Goban) {
*self.imp().board_state.borrow_mut() = board_state;
self.queue_draw();
}
let background = ImageReader::new(Cursor::new(wood_texture))
.with_guessed_format()
.unwrap()
.decode();
let background = background.map(|background| {
Pixbuf::from_bytes(
&glib::Bytes::from(background.as_bytes()),
gtk::gdk_pixbuf::Colorspace::Rgb,
false,
8,
background.width() as i32,
background.height() as i32,
background.to_rgb8().sample_layout().height_stride as i32,
)
.scale_simple(WIDTH, HEIGHT, InterpType::Nearest)
});
fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) {
let background = self
.imp()
.resource_manager
.borrow()
.as_ref()
.and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/wood_texture.jpg"));
let black_texture = self
.imp()
.resource_manager
.borrow()
.as_ref()
.and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/black_stone.png"));
let white_texture = self
.imp()
.resource_manager
.borrow()
.as_ref()
.and_then(|r| r.resource("/com/luminescent-dreams/otg-gtk/white_stone.png"));
match background {
Ok(Some(ref background)) => {
Some(Resource::Image(ref background)) => {
ctx.set_source_pixbuf(background, 0., 0.);
ctx.paint().expect("paint should never fail");
}
Ok(None) | Err(_) => ctx.set_source_rgb(0.7, 0.7, 0.7),
None => ctx.set_source_rgb(0.7, 0.7, 0.7),
}
*/
ctx.set_source_rgb(0.7, 0.7, 0.7);
let _ = ctx.paint();
let board = self.imp().board_state.borrow();
@ -148,12 +143,14 @@ impl Goban {
let hspace_between = ((width - 40) as f64) / ((board.size.width - 1) as f64);
let vspace_between = ((height - 40) as f64) / ((board.size.height - 1) as f64);
let pen = Pen {
x_offset: MARGIN as f64,
y_offset: MARGIN as f64,
let pen = Pen::new(
MARGIN as f64,
MARGIN as f64,
hspace_between,
vspace_between,
};
black_texture,
white_texture,
);
(0..board.size.width).for_each(|col| {
ctx.move_to(
@ -188,8 +185,8 @@ impl Goban {
(0..board.size.height).for_each(|row| {
(0..board.size.width).for_each(|column| {
match board.stone(&Coordinate{ row, column }) {
None => {},
match board.stone(&Coordinate { row, column }) {
None => {}
Some(Color::White) => pen.stone(ctx, row, column, Color::White, None),
Some(Color::Black) => pen.stone(ctx, row, column, Color::Black, None),
}
@ -203,9 +200,37 @@ struct Pen {
y_offset: f64,
hspace_between: f64,
vspace_between: f64,
black_stone: Option<Pixbuf>,
white_stone: Option<Pixbuf>,
}
impl Pen {
fn new(
x_offset: f64,
y_offset: f64,
hspace_between: f64,
vspace_between: f64,
black_stone: Option<Resource>,
white_stone: Option<Resource>,
) -> Self {
let black_stone = match black_stone {
Some(Resource::Image(img)) => Some(img),
_ => None,
};
let white_stone = match white_stone {
Some(Resource::Image(img)) => Some(img),
_ => None,
};
Pen {
x_offset,
y_offset,
hspace_between,
vspace_between,
black_stone,
white_stone,
}
}
fn star_point(&self, context: &cairo::Context, row: u8, col: u8) {
context.arc(
self.x_offset + (col as f64) * self.hspace_between,
@ -217,20 +242,28 @@ impl Pen {
let _ = context.fill();
}
fn stone(
&self,
context: &cairo::Context,
row: u8,
col: u8,
color: Color,
liberties: Option<u8>,
) {
fn stone(&self, ctx: &cairo::Context, row: u8, col: u8, color: Color, _liberties: Option<u8>) {
let (x_loc, y_loc) = self.stone_location(row, col);
match color {
Color::White => context.set_source_rgb(0.9, 0.9, 0.9),
Color::Black => context.set_source_rgb(0.0, 0.0, 0.0),
Color::White => match self.white_stone {
Some(ref white_stone) => ctx.set_source_pixbuf(white_stone, x_loc, y_loc),
None => ctx.set_source_rgb(0.9, 0.9, 0.9),
},
Color::Black => match self.black_stone {
Some(ref black_stone) => ctx.set_source_pixbuf(black_stone, x_loc, y_loc),
None => ctx.set_source_rgb(0.0, 0.0, 0.0),
},
}
ctx.paint().expect("paint should never fail");
/*
match color {
Color::White => ctx.set_source_rgb(0.9, 0.9, 0.9),
Color::Black => ctx.set_source_rgb(0.0, 0.0, 0.0),
};
self.draw_stone(context, row, col);
self.draw_stone(ctx, row, col);
*/
/*
if let Some(liberties) = liberties {
let stone_location = self.stone_location(row, col);
context.set_source_rgb(1., 0., 1.);
@ -238,28 +271,30 @@ impl Pen {
context.move_to(stone_location.0 - 10., stone_location.1 + 10.);
let _ = context.show_text(&format!("{}", liberties));
}
*/
}
#[allow(dead_code)]
fn ghost_stone(&self, context: &cairo::Context, row: u8, col: u8, color: Color) {
fn ghost_stone(&self, ctx: &cairo::Context, row: u8, col: u8, color: Color) {
match color {
Color::White => context.set_source_rgba(0.9, 0.9, 0.9, 0.5),
Color::Black => context.set_source_rgba(0.0, 0.0, 0.0, 0.5),
Color::White => ctx.set_source_rgba(0.9, 0.9, 0.9, 0.5),
Color::Black => ctx.set_source_rgba(0.0, 0.0, 0.0, 0.5),
};
self.draw_stone(context, row, col);
self.draw_stone(ctx, row, col);
}
fn draw_stone(&self, context: &cairo::Context, row: u8, col: u8) {
fn draw_stone(&self, ctx: &cairo::Context, row: u8, col: u8) {
let radius = self.hspace_between / 2. - 2.;
let (x_loc, y_loc) = self.stone_location(row, col);
context.arc(x_loc, y_loc, radius, 0.0, 2.0 * std::f64::consts::PI);
let _ = context.fill();
ctx.arc(x_loc, y_loc, radius, 0.0, 2.0 * std::f64::consts::PI);
let _ = ctx.fill();
}
fn stone_location(&self, row: u8, col: u8) -> (f64, f64) {
let radius = self.hspace_between / 2. - 2.;
(
self.x_offset + (col as f64) * self.hspace_between,
self.y_offset + (row as f64) * self.vspace_between,
self.x_offset + (col as f64) * self.hspace_between - radius,
self.y_offset + (row as f64) * self.vspace_between - radius,
)
}
}

View File

@ -15,48 +15,42 @@ You should have received a copy of the GNU General Public License along with On
*/
use cairo::Context;
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use otg_core::Tree;
use sgf::{GameNode, GameRecord};
use std::{cell::RefCell, rc::Rc};
use uuid::Uuid;
use gtk::prelude::*;
use otg_core::GameReviewViewModel;
const WIDTH: i32 = 200;
const HEIGHT: i32 = 800;
#[derive(Default)]
pub struct ReviewTreePrivate {
record: Rc<RefCell<Option<GameRecord>>>,
tree: Rc<RefCell<Option<Tree<Uuid>>>>,
}
const RADIUS: f64 = 7.5;
const HIGHLIGHT_WIDTH: f64 = 4.;
const SPACING: f64 = 30.;
#[glib::object_subclass]
impl ObjectSubclass for ReviewTreePrivate {
const NAME: &'static str = "ReviewTree";
type Type = ReviewTree;
type ParentType = gtk::DrawingArea;
}
#[derive(Clone)]
pub struct ReviewTree {
widget: gtk::ScrolledWindow,
drawing_area: gtk::DrawingArea,
impl ObjectImpl for ReviewTreePrivate {}
impl WidgetImpl for ReviewTreePrivate {}
impl DrawingAreaImpl for ReviewTreePrivate {}
glib::wrapper! {
pub struct ReviewTree(ObjectSubclass<ReviewTreePrivate>) @extends gtk::Widget, gtk::DrawingArea;
view: GameReviewViewModel,
}
impl ReviewTree {
pub fn new(record: GameRecord) -> Self {
let s: Self = Object::new();
pub fn new(view: GameReviewViewModel) -> ReviewTree {
let drawing_area = gtk::DrawingArea::new();
let widget = gtk::ScrolledWindow::builder().child(&drawing_area).build();
*s.imp().tree.borrow_mut() = Some(Tree::from(&record.children[0]));
*s.imp().record.borrow_mut() = Some(record);
widget.set_width_request(WIDTH);
widget.set_height_request(HEIGHT);
s.set_width_request(WIDTH);
s.set_height_request(HEIGHT);
// TODO: figure out the maximum width of the tree so that we can also set a width request
drawing_area.set_height_request(view.tree_max_depth() as i32 * SPACING as i32);
s.set_draw_func({
let s = Self {
widget,
drawing_area,
view,
};
s.drawing_area.set_draw_func({
let s = s.clone();
move |_, ctx, width, height| {
s.redraw(ctx, width, height);
@ -66,170 +60,63 @@ impl ReviewTree {
s
}
pub fn redraw(&self, ctx: &Context, _width: i32, _height: i32) {
println!("redraw");
let tree: &Option<Tree<Uuid>> = &self.imp().tree.borrow();
match tree {
Some(ref tree) => {
for node in tree.bfs_iter() {
// draw a circle given the coordinates of the nodes
// I don't know the indent. How do I keep track of that? Do I track the position of
// the parent? do I need to just make it more intrinsically a part of the position
// code?
ctx.set_source_rgb(0.7, 0.7, 0.7);
let (row, column) = tree.position(node.id);
println!("[{}] {} x {}", node.id, row, column);
let y = (row as f64) * 20. + 10.;
let x = (column as f64) * 20. + 10.;
ctx.arc(x, y, 5., 0., 2. * std::f64::consts::PI);
let _ = ctx.stroke();
}
pub fn queue_draw(&self) {
self.drawing_area.queue_draw();
}
fn redraw(&self, ctx: &Context, _width: i32, _height: i32) {
#[allow(deprecated)]
let context = WidgetExt::style_context(&self.widget);
#[allow(deprecated)]
let foreground_color = context.lookup_color("sidebar_fg_color").unwrap();
#[allow(deprecated)]
let accent_color = context.lookup_color("accent_color").unwrap();
self.view.map_tree(move |node, current| {
let parent = node.parent();
ctx.set_source_rgb(
foreground_color.red().into(),
foreground_color.green().into(),
foreground_color.blue().into(),
);
let (row, column) = node.data().position();
let y = (row as f64) * SPACING + RADIUS * 2.;
let x = (column as f64) * SPACING + RADIUS * 2.;
ctx.arc(x, y, RADIUS, 0., 2. * std::f64::consts::PI);
let _ = ctx.fill();
if let Some(parent) = parent {
ctx.set_line_width(1.);
let (row, column) = parent.data().position();
let py = (row as f64) * SPACING + RADIUS * 2.;
let px = (column as f64) * SPACING + RADIUS * 2.;
ctx.move_to(px, py);
ctx.line_to(x, y);
let _ = ctx.stroke();
}
None => {
// if there is no tree present, then there's nothing to draw!
if current == Some(node.data().game_node_id) {
ctx.set_line_width(HIGHLIGHT_WIDTH);
ctx.set_source_rgb(
accent_color.red().into(),
accent_color.green().into(),
accent_color.blue().into(),
);
ctx.arc(
x,
y,
RADIUS + HIGHLIGHT_WIDTH / 2.,
0.,
2. * std::f64::consts::PI,
);
let _ = ctx.stroke();
ctx.set_line_width(2.);
}
}
});
}
pub fn widget(&self) -> gtk::Widget {
self.widget.clone().upcast::<gtk::Widget>()
}
}
// https://llimllib.github.io/pymag-trees/
// I want to take advantage of the Wetherell Shannon algorithm, but I want some variations. In
// their diagram, they got a tree that looks like this.
//
// O
// |\
// O O
// |\ \ \
// O O O O
// |\ |\
// O O O O
//
// In the same circumstance, what I want is this:
//
// O--
// | \
// O O
// |\ |\
// O O O O
// |\
// O O
//
// In order to keep things from being overly smooshed, I want to ensure that if a branch overlaps
// with another branch, there is some extra drawing space. This might actually be similar to adding
// the principal that "A parent should be centered over its children".
//
// So, given a tree, I need to know how many children exist at each level. Then I build parents
// atop the children. At level 3, I have four children, and that happens to be the maximum width of
// the graph.
//
// A bottom-up traversal:
// - Figure out the number of nodes at each depth
/*
struct Tree {
width: Vec<usize>, // the total width of the tree at each depth
}
*/
#[cfg(test)]
mod test {
use super::*;
use sgf::{Color, GameNode, Move, MoveNode};
#[test]
fn it_calculates_width_for_single_node() {
let node = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
assert_eq!(node_width(&node), 1);
}
#[test]
fn it_calculates_width_for_node_with_children() {
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_b = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
let node_c = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
let node_d = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
node_a.children.push(node_b);
node_a.children.push(node_c);
node_a.children.push(node_d);
assert_eq!(node_width(&GameNode::MoveNode(node_a)), 3);
}
// A
// B E
// C D
#[test]
fn it_calculates_width_with_one_deep_child() {
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let mut node_b = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_c = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_d = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_e = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
node_b.children.push(GameNode::MoveNode(node_c));
node_b.children.push(GameNode::MoveNode(node_d));
assert_eq!(node_width(&GameNode::MoveNode(node_b.clone())), 2);
node_a.children.push(GameNode::MoveNode(node_b));
node_a.children.push(GameNode::MoveNode(node_e));
assert_eq!(node_width(&GameNode::MoveNode(node_a)), 3);
}
// A
// B G H
// C I
// D E F
#[test]
fn it_calculates_a_complex_tree() {
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let mut node_b = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let mut node_c = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_d = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_e = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_f = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_g = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let mut node_h = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_i = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
node_c.children.push(GameNode::MoveNode(node_d));
node_c.children.push(GameNode::MoveNode(node_e));
node_c.children.push(GameNode::MoveNode(node_f));
assert_eq!(node_width(&GameNode::MoveNode(node_c.clone())), 3);
node_b.children.push(GameNode::MoveNode(node_c));
assert_eq!(node_width(&GameNode::MoveNode(node_b.clone())), 3);
node_h.children.push(GameNode::MoveNode(node_i));
node_a.children.push(GameNode::MoveNode(node_b));
node_a.children.push(GameNode::MoveNode(node_g));
node_a.children.push(GameNode::MoveNode(node_h));
// This should be 4 if I were collapsing levels correctly, but it is 5 until I return to
// figure that step out.
assert_eq!(node_width(&GameNode::MoveNode(node_a.clone())), 5);
}
#[test]
fn a_nodes_children_get_separate_columns() {
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
let node_b = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
let node_c = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
let node_d = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
node_a.children.push(node_b.clone());
node_a.children.push(node_c.clone());
node_a.children.push(node_d.clone());
assert_eq!(
node_children_columns(&GameNode::MoveNode(node_a)),
vec![0, 1, 2]
);
}
#[test]
fn text_renderer() {
assert!(false);
}
}

View File

@ -21,10 +21,12 @@ pub use app_window::AppWindow;
mod views;
use async_std::task::{yield_now};
use otg_core::{Core, Observable, CoreRequest, CoreResponse};
use std::{rc::Rc};
use async_std::task::yield_now;
use gio::resources_lookup_data;
use gtk::gdk_pixbuf::{Colorspace, InterpType, Pixbuf};
use image::{io::Reader as ImageReader, ImageError};
use otg_core::{Core, CoreRequest, CoreResponse, Observable};
use std::{cell::RefCell, collections::HashMap, io::Cursor, rc::Rc};
#[derive(Clone)]
pub struct CoreApi {
@ -37,6 +39,94 @@ impl CoreApi {
}
}
#[derive(Clone)]
pub enum Resource {
Image(Pixbuf),
}
#[derive(Clone)]
pub struct ResourceManager {
resources: Rc<RefCell<HashMap<String, Resource>>>,
}
impl Default for ResourceManager {
fn default() -> Self {
let mut resources = HashMap::new();
for (path, xres, yres, transparency) in [
(
"/com/luminescent-dreams/otg-gtk/wood_texture.jpg",
840,
840,
false,
),
(
"/com/luminescent-dreams/otg-gtk/black_stone.png",
40,
40,
true,
),
(
"/com/luminescent-dreams/otg-gtk/white_stone.png",
40,
40,
true,
),
] {
match perftrace(&format!("loading {}", path), || {
Self::load_image(path, transparency, xres, yres)
}) {
Ok(Some(image)) => {
resources.insert(path.to_owned(), Resource::Image(image));
}
Ok(None) => println!("no image in resource bundle for {}", path),
Err(err) => println!("failed to load image {}: {}", path, err),
}
}
Self {
resources: Rc::new(RefCell::new(resources)),
}
}
}
impl ResourceManager {
pub fn resource(&self, path: &str) -> Option<Resource> {
self.resources.borrow().get(path).cloned()
}
fn load_image(
path: &str,
transparency: bool,
width: i32,
height: i32,
) -> Result<Option<Pixbuf>, ImageError> {
let image_bytes = resources_lookup_data(path, gio::ResourceLookupFlags::NONE).unwrap();
let image = ImageReader::new(Cursor::new(image_bytes))
.with_guessed_format()
.unwrap()
.decode();
image.map(|image| {
let stride = if transparency {
image.to_rgba8().sample_layout().height_stride
} else {
image.to_rgb8().sample_layout().height_stride
};
Pixbuf::from_bytes(
&glib::Bytes::from(image.as_bytes()),
Colorspace::Rgb,
transparency,
8,
image.width() as i32,
image.height() as i32,
stride as i32,
)
.scale_simple(width, height, InterpType::Nearest)
})
}
}
pub fn perftrace<F, A>(trace_name: &str, f: F) -> A
where
F: FnOnce() -> A,

View File

@ -4,8 +4,7 @@ use async_std::task::spawn;
use gio::ActionEntry;
use otg_core::{Config, ConfigOption, Core, CoreNotification, LibraryPath, Observable};
use otg_gtk::{
AppWindow,
CoreApi,
AppWindow, CoreApi, ResourceManager
};
@ -123,8 +122,9 @@ fn main() {
app.connect_activate({
move |app| {
let resources = ResourceManager::default();
let core_api = CoreApi { core: core.clone() };
let app_window = AppWindow::new(app, core_api);
let app_window = AppWindow::new(app, core_api, resources);
setup_app_configuration_action(app, app_window.clone());

View File

@ -22,16 +22,21 @@ You should have received a copy of the GNU General Public License along with On
// I'll get all of the information about the game from the core, and then render everything in the
// UI. So this will be a heavy lift on the UI side.
use crate::{components::{Goban, PlayerCard, ReviewTree}, CoreApi};
use glib::Object;
use gtk::{prelude::*, subclass::prelude::*};
use otg_core::Color;
use sgf::GameRecord;
use std::{cell::RefCell, rc::Rc};
use crate::{
components::{Goban, PlayerCard, ReviewTree},
ResourceManager,
};
use glib::Propagation;
use gtk::{gdk::Key, prelude::*, EventControllerKey};
use otg_core::{Color, GameReviewViewModel};
/*
#[derive(Default)]
pub struct GameReviewPrivate {}
pub struct GameReviewPrivate {
model: Rc<RefCell<Option<GameReviewViewModel>>>,
}
#[glib::object_subclass]
impl ObjectSubclass for GameReviewPrivate {
@ -49,16 +54,79 @@ glib::wrapper! {
}
impl GameReview {
pub fn new(_api: CoreApi, record: GameRecord) -> Self {
pub fn new(_api: CoreApi, record: GameRecord, resources: ResourceManager) -> Self {
let s: Self = Object::builder().build();
s
}
}
*/
#[derive(Clone)]
pub struct GameReview {
widget: gtk::Box,
goban: Rc<RefCell<Option<Goban>>>,
review_tree: Rc<RefCell<Option<ReviewTree>>>,
resources: ResourceManager,
view: Rc<RefCell<GameReviewViewModel>>,
}
impl GameReview {
pub fn new(view: GameReviewViewModel, resources: ResourceManager) -> Self {
let widget = gtk::Box::builder().build();
let view = Rc::new(RefCell::new(view));
let s = Self {
widget,
goban: Default::default(),
review_tree: Default::default(),
resources,
view,
};
let keypress_controller = EventControllerKey::new();
keypress_controller.connect_key_pressed({
let s = s.clone();
move |_, key, _, _| {
let mut view = s.view.borrow_mut();
match key {
Key::Down => view.next_move(),
Key::Up => view.previous_move(),
Key::Left => view.previous_variant(),
Key::Right => view.next_variant(),
_ => {
return Propagation::Proceed;
}
}
match *s.goban.borrow_mut() {
Some(ref mut goban) => goban.set_board_state(view.game_view()),
None => {}
};
match *s.review_tree.borrow() {
Some(ref tree) => tree.queue_draw(),
None => {}
}
Propagation::Stop
}
});
s.widget.add_controller(keypress_controller);
s.render();
s
}
fn render(&self) {
// It's actually really bad to be just throwing away errors. Panics make everyone unhappy.
// This is not a fatal error, so I'll replace this `unwrap` call with something that
// renders the board and notifies the user of a problem that cannot be resolved.
let board_repr = otg_core::Goban::default()
.apply_moves(record.mainline())
.unwrap();
let board = Goban::new(board_repr);
let board_repr = self.view.borrow().game_view();
let board = Goban::new(board_repr, self.resources.clone());
/*
s.attach(&board, 0, 0, 2, 2);
@ -75,7 +143,7 @@ impl GameReview {
// The review tree needs to know the record for being able to render all of the nodes. Once
// keyboard input is being handled, the tree will have to be updated on each keystroke in
// order to show the user where they are within the game record.
let review_tree = ReviewTree::new(record.clone());
let review_tree = ReviewTree::new(self.view.borrow().clone());
// I think most keyboard focus is going to end up being handled here in GameReview, as
// keystrokes need to affect both the goban and the review tree simultanesouly. Possibly
@ -87,14 +155,24 @@ impl GameReview {
.spacing(4)
.build();
player_information_section.append(&PlayerCard::new(Color::Black, &record.black_player));
player_information_section.append(&PlayerCard::new(Color::White, &record.white_player));
player_information_section
.append(&PlayerCard::new(Color::Black, &self.view.borrow().black_player()));
player_information_section
.append(&PlayerCard::new(Color::White, &self.view.borrow().white_player()));
s.append(&board);
self.widget.append(&board);
sidebar.append(&player_information_section);
sidebar.append(&review_tree);
s.append(&sidebar);
sidebar.append(&review_tree.widget());
self.widget.append(&sidebar);
s
*self.goban.borrow_mut() = Some(board);
*self.review_tree.borrow_mut() = Some(review_tree);
}
fn redraw(&self) {
}
pub fn widget(&self) -> gtk::Widget {
self.widget.clone().upcast::<gtk::Widget>()
}
}

View File

@ -1,3 +1,3 @@
[toolchain]
channel = "1.77.0"
targets = [ "wasm32-unknown-unknown" ]
targets = [ "wasm32-unknown-unknown", "thumbv6m-none-eabi" ]

View File

@ -9,6 +9,7 @@ edition = "2021"
chrono = { version = "0.4", features = [ "serde" ] }
nom = { version = "7" }
serde = { version = "1", features = [ "derive" ] }
nary_tree = { version = "0.4" }
thiserror = { version = "1"}
typeshare = { version = "1" }
uuid = { version = "0.8", features = ["v4", "serde"] }

View File

@ -2,8 +2,15 @@ use crate::{
parser::{self, Annotation, Evaluation, Move, SetupInstr, Size, UnknownProperty},
Color, Date, GameResult, GameType,
};
use nary_tree::{NodeId, NodeMut, NodeRef, Tree};
use serde::{Deserialize, Serialize};
use std::{collections::HashSet, time::Duration};
use std::{
collections::{HashMap, HashSet, VecDeque},
fmt,
fmt::Debug,
ops::{Deref, DerefMut},
time::Duration,
};
use uuid::Uuid;
#[derive(Clone, Debug, PartialEq)]
@ -32,7 +39,7 @@ pub enum SetupNodeError {
#[derive(Clone, Debug, PartialEq)]
pub enum GameNodeError {
UnsupportedGameNode(MoveNodeError, SetupNodeError),
UnsupportedGameNode(MoveNodeError, SetupNodeError, parser::Node),
ConflictingProperty,
ConflictingPosition,
}
@ -52,7 +59,7 @@ pub struct Player {
/// syntax issues, the result of the GameRecord is to have a fully-understood game. However, this
/// doesn't (yet?) go quite to the level of apply the game type (i.e., this is Go, Chess, Yinsh, or
/// whatever).
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
#[derive(Clone, Debug, PartialEq)]
pub struct GameRecord {
pub game_type: GameType,
@ -78,7 +85,7 @@ pub struct GameRecord {
pub overtime: Option<String>,
pub transcriber: Option<String>,
pub children: Vec<GameNode>,
pub trees: Vec<GameTree>,
}
impl GameRecord {
@ -111,55 +118,40 @@ impl GameRecord {
overtime: None,
transcriber: None,
children: vec![],
trees: vec![],
}
}
pub fn nodes(&self) -> Vec<&GameNode> {
self.iter().collect()
}
pub fn iter(&self) -> impl Iterator<Item = &'_ GameNode> {
self.trees
.iter()
.flat_map(|tree| tree.root().unwrap().traverse_pre_order())
.map(|nr| nr.data())
}
/// Generate a list of moves which constitute the main line of the game. This is the game as it
/// was actually played out, and by convention consists of the first node in each list of
/// children.
pub fn mainline(&self) -> Vec<&GameNode> {
let mut moves: Vec<&GameNode> = vec![];
let mut next = self.children.get(0);
while let Some(node) = next {
// Given that I know that I have a node, and I know that I'm going to push a reference
// to it onto my final list, I want to get the first of its children. And I want to
// keep doing that until there are no more first children.
//
// Just going to push references onto the list. No need to copy the nodes for this.
//
// Pushing a reference onto the list implicitely clones the reference, but not the data
// it is pointing to. This means that each time through the loop, `next` points to
// something else. This isn't being described very well, though, so it's worth
// reviewing in the future.
moves.push(node);
next = match node {
GameNode::MoveNode(node) => node.children.get(0),
GameNode::SetupNode(node) => node.children.get(0),
};
pub fn mainline(&self) -> Option<impl Iterator<Item = NodeRef<'_, GameNode>>> {
if !self.trees.is_empty() {
Some(MainlineIter {
next: self.trees[0].root(),
tree: &self.trees[0],
})
} else {
None
}
moves
}
}
impl Node for GameRecord {
fn children<'a>(&'a self) -> Vec<&'a GameNode> {
self.children.iter().collect::<Vec<&'a GameNode>>()
}
fn add_child(&mut self, node: GameNode) -> &mut GameNode {
self.children.push(node);
self.children.last_mut().unwrap()
}
}
impl TryFrom<&parser::Tree> for GameRecord {
impl TryFrom<parser::Tree> for GameRecord {
type Error = GameError;
fn try_from(tree: &parser::Tree) -> Result<Self, Self::Error> {
fn try_from(tree: parser::Tree) -> Result<Self, Self::Error> {
let mut ty = None;
let mut size = None;
let mut black_player = Player {
@ -234,6 +226,7 @@ impl TryFrom<&parser::Tree> for GameRecord {
}
}
/*
s.children = tree
.root
.next
@ -241,33 +234,205 @@ impl TryFrom<&parser::Tree> for GameRecord {
.map(GameNode::try_from)
.collect::<Result<Vec<GameNode>, GameNodeError>>()
.map_err(GameError::InvalidGameNode)?;
*/
s.trees = tree
.root
.next
.into_iter()
.map(recursive_tree_to_slab_tree)
.collect::<Result<Vec<GameTree>, GameError>>()?;
Ok(s)
}
}
fn recursive_tree_to_slab_tree(node: parser::Node) -> Result<GameTree, GameError> {
let mut slab = Tree::new();
let mut nodes: VecDeque<(NodeId, parser::Node)> = VecDeque::new();
let root_id =
slab.set_root(GameNode::try_from(node.clone()).map_err(GameError::InvalidGameNode)?);
nodes.push_back((root_id, node));
// I need to keep track of the current parent, and I need to keep on digging deeper into the
// tree. Given that I have the root, I can then easily find out all of the children.
//
// So, maybe I take the list of children. Assign each one of them to a place in the slab tree.
// Then push the child *and* its ID into a dequeue. So long as the dequeue is not empty, I want
// to pop a node and its ID from the dequeue. The retrieve the NodeMut for it and work on the
// node's children.
while let Some((node_id, node)) = nodes.pop_front() {
let mut game_node: NodeMut<GameNode> = slab
.get_mut(node_id)
.expect("invalid node_id when retrieving nodes from the game");
// I have a node that is in the tree. Now run across all of its children, adding each one
// to the tree and pushing them into the deque along with their IDs.
for child in node.next {
let slab_child = game_node
.append(GameNode::try_from(child.clone()).map_err(GameError::InvalidGameNode)?);
nodes.push_back((slab_child.node_id(), child));
}
}
Ok(GameTree(slab))
}
#[derive(Default)]
pub struct TreeIter<'a> {
queue: VecDeque<NodeRef<'a, &'a GameNode>>,
}
/*
impl<'a> Default for TreeIter<'a> {
fn default() -> Self {
TreeIter {
queue: VecDeque::default(),
}
}
}
*/
impl<'a> Iterator for TreeIter<'a> {
type Item = &'a GameNode;
fn next(&mut self) -> Option<Self::Item> {
let retval = self.queue.pop_front();
if let Some(ref retval) = retval {
retval
.children()
.for_each(|node| self.queue.push_back(node));
}
retval.map(|rv| *rv.data())
}
}
pub struct GameTree(Tree<GameNode>);
impl Default for GameTree {
fn default() -> Self {
Self(Tree::new())
}
}
impl Clone for GameTree {
fn clone(&self) -> Self {
match self.0.root() {
None => Self(Tree::new()),
Some(source_root_node) => {
let mut dest = Tree::new();
let dest_root_id = dest.set_root(source_root_node.data().clone());
// In order to add a node to the new tree, I need to know the ID of the parent in
// the source tree and the ID of the parent in the destination tree. So I want a
// lookup table that maps source IDs to destination IDs. But is that sufficient?
// Perhaps I can just keep a mapping from a source noderef to a destination ID.
// I don't think I can keep more than one mutable destination node.
let mut mapping: HashMap<NodeId, NodeId> = HashMap::new();
mapping.insert(source_root_node.node_id(), dest_root_id);
for source_node in source_root_node.traverse_level_order() {
match source_node.parent() {
None => {}
Some(parent) => {
let source_node_parent_id = parent.node_id();
let target_node_parent_id = mapping.get(&source_node_parent_id).expect("node should have been added to the source to dest mapping when being cloned");
let mut parent = dest.get_mut(*target_node_parent_id).expect(
"destination parent node to exist before reaching potential children",
);
let dest_id = parent.append(source_node.data().clone()).node_id();
mapping.insert(source_node.node_id(), dest_id);
}
}
}
Self(dest)
}
}
}
}
impl Debug for GameTree {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
self.write_formatted(f)
}
}
impl Deref for GameTree {
type Target = Tree<GameNode>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for GameTree {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl PartialEq for GameTree {
fn eq(&self, other: &Self) -> bool {
// Get pre-order iterators over both trees, zip them, and ensure that the data contents are
// the same between them
let left_root = self.root();
let right_root = other.root();
match (left_root, right_root) {
(Some(left_root), Some(right_root)) => {
for (left_node, right_node) in std::iter::zip(
left_root.traverse_pre_order(),
right_root.traverse_pre_order(),
) {
if left_node.data() != right_node.data() {
return false;
}
}
}
(None, None) => return true,
_ => return false,
}
true
}
}
pub struct MainlineIter<'a> {
next: Option<NodeRef<'a, GameNode>>,
tree: &'a Tree<GameNode>,
}
impl<'a> Iterator for MainlineIter<'a> {
type Item = NodeRef<'a, GameNode>;
fn next(&mut self) -> Option<Self::Item> {
if let Some(next) = self.next.take() {
let ret = self.tree.get(next.node_id())?;
self.next = next
.first_child()
.and_then(|child| self.tree.get(child.node_id()));
Some(ret)
} else {
None
}
}
}
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub enum GameNode {
MoveNode(MoveNode),
SetupNode(SetupNode),
}
pub trait Node {
/// Provide a pre-order traversal of all of the nodes in the game tree.
fn nodes<'a>(&'a self) -> Vec<&'a GameNode> {
self.children()
.iter()
.flat_map(|node| {
let mut children = node.nodes();
let mut v = vec![*node];
v.append(&mut children);
v
})
.collect::<Vec<&'a GameNode>>()
impl fmt::Display for GameNode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
match self {
GameNode::MoveNode(_) => write!(f, "MoveNode"),
GameNode::SetupNode(_) => write!(f, "SetupNode"),
}
}
fn children(&self) -> Vec<&GameNode>;
fn add_child(&mut self, node: GameNode) -> &mut GameNode;
}
impl GameNode {
@ -279,70 +444,20 @@ impl GameNode {
}
}
impl Node for GameNode {
fn children(&self) -> Vec<&GameNode> {
match self {
GameNode::MoveNode(node) => node.children(),
GameNode::SetupNode(node) => node.children(),
}
}
fn nodes(&self) -> Vec<&GameNode> {
match self {
GameNode::MoveNode(node) => node.nodes(),
GameNode::SetupNode(node) => node.nodes(),
}
}
fn add_child(&mut self, new_node: GameNode) -> &mut GameNode {
match self {
GameNode::MoveNode(node) => node.add_child(new_node),
GameNode::SetupNode(node) => node.add_child(new_node),
}
}
}
impl TryFrom<&parser::Node> for GameNode {
impl TryFrom<parser::Node> for GameNode {
type Error = GameNodeError;
fn try_from(n: &parser::Node) -> Result<Self, Self::Error> {
// I originally wrote this recursively. However, on an ordinary game of a couple hundred
// moves, that meant that I was recursing 500 functions, and that exceeded the stack limit.
// So, instead, I need to unroll everything to non-recursive form.
//
// So, I can treat each branch of the tree as a single line. Iterate over that line. I can
// only use the MoveNode::try_from and SetupNode::try_from if those functions don't
// recurse. Instead, I'm going to process just that node, then return to here and process
// the children.
let move_node = MoveNode::try_from(n);
let setup_node = SetupNode::try_from(n);
fn try_from(n: parser::Node) -> Result<Self, Self::Error> {
let move_node = MoveNode::try_from(n.clone());
let setup_node = SetupNode::try_from(n.clone());
// I'm much too tired when writing this. I'm still recursing, but I did cut the number of
// recursions in half. This helps, but it still doesn't guarantee that I'm going to be able
// to parse all possible games. So, still, treat each branch of the game as a single line.
// Iterate over that line, don't recurse. Create bookmarks at each branch point, and then
// come back to each one.
let children = n
.next
.iter()
.map(GameNode::try_from)
.collect::<Result<Vec<Self>, Self::Error>>()?;
let node = match (move_node, setup_node) {
(Ok(mut node), _) => {
node.children = children;
Ok(Self::MoveNode(node))
}
(Err(_), Ok(mut node)) => {
node.children = children;
Ok(Self::SetupNode(node))
}
match (move_node, setup_node) {
(Ok(node), _) => Ok(Self::MoveNode(node)),
(Err(_), Ok(node)) => Ok(Self::SetupNode(node)),
(Err(move_err), Err(setup_err)) => {
Err(Self::Error::UnsupportedGameNode(move_err, setup_err))
Err(Self::Error::UnsupportedGameNode(move_err, setup_err, n))
}
}?;
Ok(node)
}
}
}
@ -351,7 +466,6 @@ pub struct MoveNode {
pub id: Uuid,
pub color: Color,
pub mv: Move,
pub children: Vec<GameNode>,
pub time_left: Option<Duration>,
pub moves_left: Option<usize>,
@ -369,7 +483,6 @@ impl MoveNode {
id: Uuid::new_v4(),
color,
mv,
children: Vec::new(),
time_left: None,
moves_left: None,
@ -383,21 +496,10 @@ impl MoveNode {
}
}
impl Node for MoveNode {
fn children<'a>(&'a self) -> Vec<&'a GameNode> {
self.children.iter().collect::<Vec<&'a GameNode>>()
}
fn add_child(&mut self, node: GameNode) -> &mut GameNode {
self.children.push(node);
self.children.last_mut().unwrap()
}
}
impl TryFrom<&parser::Node> for MoveNode {
impl TryFrom<parser::Node> for MoveNode {
type Error = MoveNodeError;
fn try_from(n: &parser::Node) -> Result<Self, Self::Error> {
fn try_from(n: parser::Node) -> Result<Self, Self::Error> {
let s = match n.mv() {
Some((color, mv)) => {
let mut s = Self::new(color, mv);
@ -460,7 +562,6 @@ pub struct SetupNode {
id: Uuid,
pub positions: Vec<parser::SetupInstr>,
pub children: Vec<GameNode>,
}
impl SetupNode {
@ -480,26 +581,14 @@ impl SetupNode {
Ok(Self {
id: Uuid::new_v4(),
positions,
children: Vec::new(),
})
}
}
impl Node for SetupNode {
fn children<'a>(&'a self) -> Vec<&'a GameNode> {
self.children.iter().collect::<Vec<&'a GameNode>>()
}
#[allow(dead_code)]
fn add_child(&mut self, _node: GameNode) -> &mut GameNode {
unimplemented!()
}
}
impl TryFrom<&parser::Node> for SetupNode {
impl TryFrom<parser::Node> for SetupNode {
type Error = SetupNodeError;
fn try_from(n: &parser::Node) -> Result<Self, Self::Error> {
fn try_from(n: parser::Node) -> Result<Self, Self::Error> {
match n.setup() {
Some(elements) => Self::new(elements),
None => Err(Self::Error::NotASetupNode),
@ -507,6 +596,7 @@ impl TryFrom<&parser::Node> for SetupNode {
}
}
/*
#[allow(dead_code)]
pub fn path_to_node(node: &GameNode, id: Uuid) -> Vec<&GameNode> {
if node.id() == id {
@ -523,6 +613,7 @@ pub fn path_to_node(node: &GameNode, id: Uuid) -> Vec<&GameNode> {
Vec::new()
}
*/
#[cfg(test)]
mod test {
@ -543,6 +634,7 @@ mod test {
assert_eq!(tree.nodes().len(), 0);
}
/*
#[test]
fn it_can_add_moves_to_a_game() {
let mut game = GameRecord::new(
@ -555,16 +647,21 @@ mod test {
Player::default(),
);
/*
let first_move = MoveNode::new(Color::Black, Move::Move("dd".to_owned()));
let first_ = game.add_child(GameNode::MoveNode(first_move.clone()));
let second_move = MoveNode::new(Color::White, Move::Move("qq".to_owned()));
first_.add_child(GameNode::MoveNode(second_move.clone()));
*/
/*
let nodes = game.nodes();
assert_eq!(nodes.len(), 2);
assert_eq!(nodes[0].id(), first_move.id);
assert_eq!(nodes[1].id(), second_move.id);
*/
}
*/
#[ignore]
#[test]
@ -588,7 +685,7 @@ mod test {
],
next: vec![],
};
assert_matches!(GameNode::try_from(&n), Ok(GameNode::MoveNode(_)));
assert_matches!(GameNode::try_from(n), Ok(GameNode::MoveNode(_)));
}
}
@ -630,10 +727,10 @@ mod move_node_tests {
],
next: vec![],
};
assert_matches!(MoveNode::try_from(&n), Ok(node) => {
assert_matches!(MoveNode::try_from(n), Ok(node) => {
assert_eq!(node.color, Color::White);
assert_eq!(node.mv, Move::Move("dp".to_owned()));
assert_eq!(node.children, vec![]);
// assert_eq!(node.children, vec![]);
assert_eq!(node.time_left, Some(Duration::from_secs(176)));
assert_eq!(node.comments, Some("Comments in the game".to_owned()));
});
@ -653,7 +750,7 @@ mod move_node_tests {
next: vec![],
};
assert_matches!(
MoveNode::try_from(&n),
MoveNode::try_from(n),
Err(MoveNodeError::IncompatibleProperty(_))
);
}
@ -703,7 +800,7 @@ mod path_test {
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap();
let games = games
.into_iter()
.map(|game| GameRecord::try_from(&game).expect("game to parse"))
.map(|game| GameRecord::try_from(game).expect("game to parse"))
.collect::<Vec<GameRecord>>();
f(games);
}
@ -722,7 +819,11 @@ mod path_test {
|games| {
let game = &games[0];
let moves = game.mainline();
let moves = game
.mainline()
.expect("there should be a mainline in this file")
.map(|nr| nr.data())
.collect::<Vec<&GameNode>>();
assert_matches!(moves[0], GameNode::MoveNode(node) => {
assert_eq!(node.color, Color::Black);
assert_eq!(node.mv, Move::Move("pp".to_owned()));
@ -744,7 +845,11 @@ mod path_test {
with_file(std::path::Path::new("test_data/branch_test.sgf"), |games| {
let game = &games[0];
let moves = game.mainline();
let moves = game
.mainline()
.expect("there should be a mainline in this file")
.map(|nr| nr.data())
.collect::<Vec<&GameNode>>();
assert_matches!(moves[1], GameNode::MoveNode(node) => {
assert_eq!(node.color, Color::White);
assert_eq!(node.mv, Move::Move("dd".to_owned()));
@ -791,7 +896,7 @@ mod file_test {
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap();
let games = games
.into_iter()
.map(|game| GameRecord::try_from(&game).expect("game to parse"))
.map(|game| GameRecord::try_from(game).expect("game to parse"))
.collect::<Vec<GameRecord>>();
f(games);
}
@ -875,6 +980,7 @@ mod file_test {
}
*/
/*
let children = game.children();
let node = children.first().unwrap();
assert_matches!(node, GameNode::MoveNode(node) => {
@ -892,6 +998,7 @@ mod file_test {
assert_eq!(node.time_left, Some(Duration::from_secs(1765)));
assert_eq!(node.comments, None);
});
*/
/*
let node = node.next().unwrap();
let expected_properties = vec![
@ -911,4 +1018,39 @@ mod file_test {
},
);
}
#[test]
fn it_can_load_a_file_with_multiple_roots() {
with_file(std::path::Path::new("test_data/multi-tree.sgf"), |games| {
assert_eq!(games.len(), 1);
let game = &games[0];
assert_eq!(game.game_type, GameType::Go);
assert_eq!(
game.board_size,
Size {
width: 19,
height: 19
}
);
assert_eq!(game.trees.len(), 2);
assert_matches!(game.trees[0].root().unwrap().data(), GameNode::MoveNode(node) => {
assert_eq!(node.color, Color::Black);
assert_eq!(node.mv, Move::Move("pd".to_owned()));
});
assert_matches!(game.trees[1].root().unwrap().data(), GameNode::MoveNode(node) => {
assert_eq!(node.color, Color::Black);
assert_eq!(node.mv, Move::Move("pc".to_owned()));
});
});
}
#[test]
fn it_can_copy_a_game_record() {
with_file(std::path::Path::new("test_data/multi-tree.sgf"), |games| {
let dest = games.clone();
assert_eq!(games.len(), dest.len());
assert_eq!(games[0], dest[0]);
});
}
}

View File

@ -1,10 +1,10 @@
mod date;
mod game;
pub use game::{GameNode, GameRecord, MoveNode, Player};
pub use game::{GameNode, GameRecord, GameTree, MoveNode, Player};
mod parser;
pub use parser::{parse_collection, Move};
pub use parser::{parse_collection, Move, Size};
mod types;
pub use types::*;
@ -22,6 +22,7 @@ pub enum Error {
InvalidSgf(VerboseNomError),
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct VerboseNomError(nom::error::VerboseError<String>);
@ -73,7 +74,7 @@ pub fn parse_sgf(input: &str) -> Result<Vec<Result<GameRecord, game::GameError>>
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(input)?;
let games = games
.into_iter()
.map(|game| GameRecord::try_from(&game))
.map(GameRecord::try_from)
.collect::<Vec<Result<GameRecord, game::GameError>>>();
Ok(games)

View File

@ -302,10 +302,10 @@ impl Move {
Move::Move(s) => {
if s.len() == 2 {
let mut parts = s.chars();
let row_char = parts.next().unwrap();
let row = row_char as u8 - b'a';
let column_char = parts.next().unwrap();
let column = column_char as u8 - b'a';
let row_char = parts.next().unwrap();
let row = row_char as u8 - b'a';
Some((row, column))
} else {
unimplemented!("moves must contain exactly two characters");

View File

@ -56,6 +56,7 @@ pub enum Error {
InvalidSgf(VerboseNomError),
}
#[allow(dead_code)]
#[derive(Debug)]
pub struct VerboseNomError(nom::error::VerboseError<String>);

View File

@ -0,0 +1 @@
(;GM[1]FF[4]CA[UTF-8]AP[Sabaki:0.52.2]KM[7.5]SZ[19]DT[2024-04-19](;B[pd](;W[qc];B[qd];W[pc];B[oc];W[ob];B[nc];W[nb];B[mc];W[rd];B[re];W[rc];B[qf])(;W[qf];B[nc];W[rd];B[qc];W[pi]))(;B[pc];W[qe];B[oe];W[pg];B[ld];W[qj]))

View File

@ -9,6 +9,10 @@ use std::{
rc::Rc,
};
// I need to take what I learned about linked lists and about the other Tree data structure, and
// apply it here with arena allocation.
//
// Also, smarter node allocation and pointer handling in order to avoid clones.
#[derive(Clone, Debug, Default)]
pub enum Tree<T> {
#[default]
@ -55,6 +59,16 @@ impl<T> Tree<T> {
None
}
// Do a depth-first-search in order to get the path to a node. Start with a naive recursive
// implementation, then switch to a stack-based implementation in order to avoid exceeding the
// stack.
pub fn path_to<F>(&self, f: F) -> Vec<Node<T>>
where
F: FnOnce(&T) -> bool + Copy,
{
unimplemented!()
}
/// Convert each node of a tree from type T to type U
pub fn map<F, U>(&self, op: F) -> Tree<U>
where
@ -146,6 +160,13 @@ impl<T> Node<T> {
}
}
impl<T: PartialEq> PartialEq for Node<T> {
fn eq(&self, other: &Node<T>) -> bool {
self.0.borrow().value == other.0.borrow().value
&& self.0.borrow().children == other.0.borrow().children
}
}
#[cfg(test)]
mod tests {
use super::*;
@ -184,4 +205,31 @@ mod tests {
assert!(tree2.find_bfs(|val| *val == "16").is_some());
assert!(tree2.find_bfs(|val| *val == "17").is_some());
}
#[test]
fn path_to_on_empty_tree_returns_empty() {
let tree: Tree<&str> = Tree::default();
assert_eq!(tree.path_to(|val| *val == "i"), vec![]);
}
// A
// B G H
// C I
// D E F
#[test]
fn it_can_find_a_path_to_a_node() {
let (tree, a) = Tree::new("A");
let b = a.add_child_value("B");
let c = b.add_child_value("C");
let _d = c.add_child_value("D");
let _e = c.add_child_value("D");
let _f = c.add_child_value("D");
let _g = a.add_child_value("G");
let h = a.add_child_value("H");
let i = a.add_child_value("I");
assert_eq!(tree.path_to(|val| *val == "z"), vec![]);
assert_eq!(tree.path_to(|val| *val == "i"), vec![a, h, i]);
}
}