Compare commits
102 Commits
kifu/local
...
main
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | 5d04c84437 | |
Savanni D'Gerinel | 791f2be3c5 | |
Savanni D'Gerinel | 74b7f1c6f7 | |
Savanni D'Gerinel | 9c490a84a4 | |
Savanni D'Gerinel | 724cc1a3f0 | |
Savanni D'Gerinel | 8f71760604 | |
Savanni D'Gerinel | 11abde345e | |
Savanni D'Gerinel | a5b76c8171 | |
Savanni D'Gerinel | 9b23dd5acd | |
Savanni D'Gerinel | 54225ca729 | |
Savanni D'Gerinel | 95b46de7fc | |
Savanni D'Gerinel | caaf9c57c6 | |
Savanni D'Gerinel | 81d452694d | |
Savanni D'Gerinel | 88cf32047b | |
Savanni D'Gerinel | 6cae7dbb0e | |
Savanni D'Gerinel | 80776c65d8 | |
Savanni D'Gerinel | 1c54e0832b | |
Savanni D'Gerinel | aee4528fb3 | |
Savanni D'Gerinel | 0535b6da5a | |
Savanni D'Gerinel | b55324feab | |
Savanni D'Gerinel | 50d8a9670e | |
Savanni D'Gerinel | 9cda35e766 | |
Savanni D'Gerinel | d0f461a5eb | |
Savanni D'Gerinel | 70c013218a | |
Savanni D'Gerinel | 37c7e04820 | |
Savanni D'Gerinel | 291663d4a3 | |
Savanni D'Gerinel | 2b0fc7639e | |
Savanni D'Gerinel | 80d8dedbaf | |
Savanni D'Gerinel | d7a70119c8 | |
Savanni D'Gerinel | 54c4b99ab6 | |
Savanni D'Gerinel | ef5415303b | |
Savanni D'Gerinel | 8d183d6d8c | |
Savanni D'Gerinel | 0b949111d2 | |
Savanni D'Gerinel | 6164cb3b39 | |
Savanni D'Gerinel | 22f0f9061c | |
Savanni D'Gerinel | 0bb5e62f96 | |
Savanni D'Gerinel | 06aedc34bb | |
Savanni D'Gerinel | 84b077e20c | |
Savanni D'Gerinel | fc2e88add2 | |
Savanni D'Gerinel | 15c4ae9bad | |
Savanni D'Gerinel | 7dd531b493 | |
Savanni D'Gerinel | cbfb3f2e37 | |
Savanni D'Gerinel | 9540a2c5bb | |
Savanni D'Gerinel | 6165d65977 | |
Savanni D'Gerinel | 4f8a1636c1 | |
Savanni D'Gerinel | 20b02fbd90 | |
Savanni D'Gerinel | 278ec27b4e | |
Savanni D'Gerinel | 8b7add37c1 | |
Savanni D'Gerinel | 5441a3c441 | |
Savanni D'Gerinel | b1374229f3 | |
Savanni D'Gerinel | bc5042c004 | |
Savanni D'Gerinel | 0534143d6b | |
Savanni D'Gerinel | d7f5269e15 | |
Savanni D'Gerinel | c913e9da37 | |
Savanni D'Gerinel | c50bd652f1 | |
Savanni D'Gerinel | 093e1f7f8a | |
Savanni D'Gerinel | 3c94f906a6 | |
Savanni D'Gerinel | 0aecaee760 | |
Savanni D'Gerinel | baeb458126 | |
Savanni D'Gerinel | da144a58ec | |
Savanni D'Gerinel | f09af67193 | |
Savanni D'Gerinel | 32391a46e7 | |
Savanni D'Gerinel | 0a62c96b0f | |
Savanni D'Gerinel | 78863ee709 | |
Savanni D'Gerinel | 5cdcf0499c | |
Savanni D'Gerinel | b982f2c1cc | |
Savanni D'Gerinel | 46b25cc6c5 | |
Savanni D'Gerinel | 9fbc630500 | |
Savanni D'Gerinel | b481d5d058 | |
Savanni D'Gerinel | 7a06b8cf39 | |
Savanni D'Gerinel | 3192c0a142 | |
Savanni D'Gerinel | acf7ca0c9a | |
Savanni D'Gerinel | 64138b9e90 | |
Savanni D'Gerinel | e587d269e9 | |
Savanni D'Gerinel | 57aadd7597 | |
Savanni D'Gerinel | b70d927eac | |
Savanni D'Gerinel | 3a7f204883 | |
Savanni D'Gerinel | 642351f248 | |
Savanni D'Gerinel | d9bb9d92e5 | |
Savanni D'Gerinel | 30e7bdb817 | |
Savanni D'Gerinel | 556f91b70b | |
Savanni D'Gerinel | 894575b0fb | |
Savanni D'Gerinel | d964ab0d2f | |
Savanni D'Gerinel | 74c8eb6861 | |
Savanni D'Gerinel | 3aac3b8393 | |
Savanni D'Gerinel | 295f0a0411 | |
Savanni D'Gerinel | e694ba74ca | |
Savanni D'Gerinel | 5f9cd2622a | |
Savanni D'Gerinel | 48271389ad | |
Savanni D'Gerinel | 49571b0f82 | |
Savanni D'Gerinel | 89a289a1ae | |
Savanni D'Gerinel | fe082773e3 | |
Savanni D'Gerinel | db9efbaedd | |
Savanni D'Gerinel | 1d959117aa | |
Savanni D'Gerinel | a5990a2a30 | |
Savanni D'Gerinel | bd6d5b62e3 | |
Savanni D'Gerinel | 82c1765513 | |
Savanni D'Gerinel | de54ec676f | |
Savanni D'Gerinel | 5612c89a61 | |
Savanni D'Gerinel | c3c144e035 | |
Savanni D'Gerinel | 05a6dcf3af | |
Savanni D'Gerinel | b98e0bdcea |
File diff suppressed because it is too large
Load Diff
|
@ -2,6 +2,9 @@
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
members = [
|
members = [
|
||||||
"authdb",
|
"authdb",
|
||||||
|
"bike-lights/bike",
|
||||||
|
"bike-lights/core",
|
||||||
|
"bike-lights/simulator",
|
||||||
"changeset",
|
"changeset",
|
||||||
"config",
|
"config",
|
||||||
"config-derive",
|
"config-derive",
|
||||||
|
@ -18,14 +21,14 @@ members = [
|
||||||
"hex-grid",
|
"hex-grid",
|
||||||
"icon-test",
|
"icon-test",
|
||||||
"ifc",
|
"ifc",
|
||||||
"kifu/core",
|
|
||||||
"kifu/gtk",
|
|
||||||
"memorycache",
|
"memorycache",
|
||||||
"nom-training",
|
"nom-training",
|
||||||
|
"otg/core",
|
||||||
|
"otg/gtk",
|
||||||
"result-extended",
|
"result-extended",
|
||||||
"screenplay",
|
"screenplay",
|
||||||
"sgf",
|
"sgf",
|
||||||
"timezone-testing",
|
"timezone-testing",
|
||||||
"tree",
|
"tree",
|
||||||
"visions/server",
|
"visions/server", "gm-dash/server",
|
||||||
]
|
]
|
||||||
|
|
|
@ -19,6 +19,8 @@ clap = { version = "4", features = [ "derive" ] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sha2 = { version = "0.10" }
|
sha2 = { version = "0.10" }
|
||||||
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite" ] }
|
sqlx = { version = "0.7", features = [ "runtime-tokio", "sqlite" ] }
|
||||||
|
# sqlformat introduced a mistaken breaking change in 0.2.7
|
||||||
|
sqlformat = { version = "=0.2.6" }
|
||||||
thiserror = { version = "1" }
|
thiserror = { version = "1" }
|
||||||
tokio = { version = "1", features = [ "full" ] }
|
tokio = { version = "1", features = [ "full" ] }
|
||||||
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
uuid = { version = "0.4", features = [ "serde", "v4" ] }
|
||||||
|
|
|
@ -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"
|
|
@ -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" }
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,158 @@
|
||||||
|
$fn = 50;
|
||||||
|
threshold = 0.1;
|
||||||
|
half_threshold = threshold / 2;
|
||||||
|
bevel = 0.5;
|
||||||
|
|
||||||
|
wire_radius = 1;
|
||||||
|
|
||||||
|
wall_thickness = 2;
|
||||||
|
cutout_threshold = 1;
|
||||||
|
|
||||||
|
battery_length = 71;
|
||||||
|
battery_width = 18.75;
|
||||||
|
|
||||||
|
cell_holder_length = battery_length + wall_thickness * 2;
|
||||||
|
cell_holder_width = battery_width + wall_thickness * 2;
|
||||||
|
cell_holder_height = battery_width + wall_thickness;
|
||||||
|
|
||||||
|
battery_contact_thickness = .6;
|
||||||
|
// battery_contact_thickness = 1;
|
||||||
|
battery_contact_width = 11;
|
||||||
|
battery_contact_length = 12.8;
|
||||||
|
battery_contact_spring_height = 10.5;
|
||||||
|
battery_contact_flange_height = 1.9;
|
||||||
|
|
||||||
|
converter_width = 11.25;
|
||||||
|
converter_length = 22.25;
|
||||||
|
converter_height = 5;
|
||||||
|
|
||||||
|
|
||||||
|
include <./common.scad>;
|
||||||
|
|
||||||
|
// box(20, 10, 10);
|
||||||
|
// color("blue", 0.5) cube([10, 20, 10], center = true);
|
||||||
|
|
||||||
|
module cell_cradle(width, height) {
|
||||||
|
difference() {
|
||||||
|
translate([0, 0, -height / 2]) cube([width,
|
||||||
|
wall_thickness,
|
||||||
|
height],
|
||||||
|
center = true);
|
||||||
|
color("red", 1) translate([0, 0, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = wall_thickness + cutout_threshold,
|
||||||
|
r = width / 2,
|
||||||
|
center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module cell_box() {
|
||||||
|
union() {
|
||||||
|
channel(cell_holder_length, cell_holder_width, cell_holder_height);
|
||||||
|
translate([0, -battery_length / 6, wall_thickness]) cell_cradle(cell_holder_width, cell_holder_height / 2);
|
||||||
|
translate([0, battery_length / 6, wall_thickness]) cell_cradle(cell_holder_width, cell_holder_height / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module contact_box() {
|
||||||
|
contact_thickness = battery_contact_flange_height * .75;
|
||||||
|
cutout_width = battery_contact_width * .8;
|
||||||
|
// box_thickness = contact_thickness_ + wall_thickness * 2;
|
||||||
|
// box_height = width + wall_thickness;
|
||||||
|
|
||||||
|
difference() {
|
||||||
|
box(wall_thickness * 2 + contact_thickness, cell_holder_width, cell_holder_height);
|
||||||
|
translate([0, contact_thickness, wall_thickness * 2])
|
||||||
|
cube([battery_contact_width,
|
||||||
|
wall_thickness * 2,
|
||||||
|
battery_contact_length + threshold],
|
||||||
|
center = true);
|
||||||
|
|
||||||
|
color("red", 1) translate([0,
|
||||||
|
-(wall_thickness + contact_thickness + threshold) / 2,
|
||||||
|
cell_holder_height / 2])
|
||||||
|
cube([5, wall_thickness + threshold * 2, cell_holder_height], center = true);
|
||||||
|
|
||||||
|
translate([0,
|
||||||
|
-(wall_thickness + contact_thickness + threshold) / 2 - wire_radius,
|
||||||
|
0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = cell_holder_width, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
color("green", 1) translate([-cell_holder_width / 2, 0, cell_holder_height / 2])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = 5, r = contact_thickness / 2, center = true);
|
||||||
|
|
||||||
|
color("green", 1) translate([cell_holder_width / 2, 0, cell_holder_height / 2])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = 5, r = contact_thickness / 2, center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module battery_slot() {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
translate([0, -cell_holder_length / 2, 0]) contact_box();
|
||||||
|
translate([0, wall_thickness, 0]) cell_box();
|
||||||
|
translate([0, cell_holder_length / 2 + wall_thickness * 2, 0])
|
||||||
|
rotate([0, 0, 180])
|
||||||
|
contact_box();
|
||||||
|
}
|
||||||
|
translate([cell_holder_width / 2, 1, 0]) rotate([90, 0, 0]) cylinder(h = cell_holder_length + wall_thickness * 4 + battery_contact_flange_height * 2, r = wire_radius, center = true);
|
||||||
|
translate([-cell_holder_width / 2, 1, 0]) rotate([90, 0, 0]) cylinder(h = cell_holder_length + wall_thickness * 4 + battery_contact_flange_height * 2, r = wire_radius, center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module converter_box() {
|
||||||
|
box_length = wall_thickness * 2 + converter_height;
|
||||||
|
box_width = cell_holder_width * 2 - wall_thickness;
|
||||||
|
difference() {
|
||||||
|
box(box_length, box_width, cell_holder_height);
|
||||||
|
|
||||||
|
translate([cell_holder_width - wire_radius, 0, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = box_length, r = wire_radius, center = true);
|
||||||
|
translate([cell_holder_width - wire_radius * 2, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = wall_thickness + threshold, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([-cell_holder_width + wire_radius, 0, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = box_length, r = wire_radius, center = true);
|
||||||
|
translate([-cell_holder_width + wire_radius * 2, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = wall_thickness + threshold, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([0, -box_length / 2, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
cylinder(h = cell_holder_width * 2 + wall_thickness, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([-cell_holder_width * .75, (-box_length + wall_thickness) / 2, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = wall_thickness * 2, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
translate([cell_holder_width * .75, (-box_length + wall_thickness) / 2, 0])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
cylinder(h = wall_thickness * 2, r = wire_radius, center = true);
|
||||||
|
|
||||||
|
color("red", 1) translate([-box_width / 4, -(converter_height + wall_thickness) / 2, cell_holder_height / 2])
|
||||||
|
cube([5, wall_thickness + threshold * 2, cell_holder_height], center = true);
|
||||||
|
color("red", 1) translate([box_width / 4, -(converter_height + wall_thickness) / 2, cell_holder_height / 2])
|
||||||
|
cube([5, wall_thickness + threshold * 2, cell_holder_height], center = true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module battery_case() {
|
||||||
|
union() {
|
||||||
|
translate([-cell_holder_width / 2, 0, 0]) battery_slot();
|
||||||
|
translate([cell_holder_width / 2 - wall_thickness, 0, 0]) battery_slot();
|
||||||
|
translate([-wall_thickness / 2,
|
||||||
|
cell_holder_length / 2 + wall_thickness * 2 + battery_contact_flange_height + wall_thickness * 2 + wall_thickness / 2,
|
||||||
|
0])
|
||||||
|
converter_box();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
battery_case();
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
module pill(length, bevel) {
|
||||||
|
hull() {
|
||||||
|
translate([0, 0, (-length / 2) + bevel]) sphere(r = bevel);
|
||||||
|
translate([0, 0, (length / 2) - bevel]) sphere(r = bevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module box_face(dimensions, bevel = 0) {
|
||||||
|
x = dimensions[0];
|
||||||
|
y = dimensions[1];
|
||||||
|
z = dimensions[2];
|
||||||
|
|
||||||
|
if (bevel > 0) {
|
||||||
|
translate([0, 0, z / 2])
|
||||||
|
hull() {
|
||||||
|
pill(z, bevel);
|
||||||
|
translate([x, 0, 0])
|
||||||
|
pill(z, bevel);
|
||||||
|
translate([x, y, 0])
|
||||||
|
pill(z, bevel);
|
||||||
|
translate([0, y, 0])
|
||||||
|
pill(z, bevel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cube(dimensions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module channel(length, width, height, bevel) {
|
||||||
|
union() {
|
||||||
|
box_face([length, width, wall_thickness], bevel);
|
||||||
|
|
||||||
|
translate([0, wall_thickness - bevel, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
box_face([length, height, wall_thickness], bevel);
|
||||||
|
|
||||||
|
translate([0, width + bevel, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
box_face([length, height, wall_thickness], bevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module box(length, width, height, bevel = 0) {
|
||||||
|
union() {
|
||||||
|
channel(length, width, height, bevel);
|
||||||
|
|
||||||
|
translate([-bevel, 0, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
box_face([width, height, wall_thickness], bevel);
|
||||||
|
translate([length - wall_thickness + bevel, 0, bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
box_face([width, height, wall_thickness], bevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
$fn = 50;
|
||||||
|
threshold = 0.1;
|
||||||
|
|
||||||
|
board_length = 92;
|
||||||
|
board_width = 72;
|
||||||
|
board_height = 21.5;
|
||||||
|
wall_thickness = 2;
|
||||||
|
bevel = 0.5;
|
||||||
|
|
||||||
|
hinge_radius = 2.5;
|
||||||
|
|
||||||
|
case_width = board_width + wall_thickness * 2;
|
||||||
|
case_length = board_length + wall_thickness * 2;
|
||||||
|
case_height = board_height + wall_thickness;
|
||||||
|
|
||||||
|
include <./common.scad>;
|
||||||
|
|
||||||
|
module hinge(length) {
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
cylinder(h = length, r = hinge_radius);
|
||||||
|
translate([0, -hinge_radius, 0])
|
||||||
|
cube([hinge_radius, hinge_radius * 2, length]);
|
||||||
|
}
|
||||||
|
translate([0, 0, -threshold / 2]) cylinder(h = length + threshold, r = 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module main_case() {
|
||||||
|
hinge_length = board_length / 4;
|
||||||
|
hinge_y_offset = board_width + wall_thickness + hinge_radius;
|
||||||
|
hinge_z_offset = board_height;
|
||||||
|
|
||||||
|
difference() {
|
||||||
|
union() {
|
||||||
|
box(case_length,
|
||||||
|
case_width,
|
||||||
|
case_height,
|
||||||
|
bevel);
|
||||||
|
|
||||||
|
translate([0, -hinge_radius - bevel + threshold, hinge_z_offset + bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
hinge(case_length / 4);
|
||||||
|
|
||||||
|
translate([case_length - hinge_length, -hinge_radius - bevel + threshold, hinge_z_offset + bevel])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 90, 0])
|
||||||
|
hinge(case_length / 4);
|
||||||
|
|
||||||
|
translate([43, case_width, wall_thickness + 8])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 180, 0])
|
||||||
|
linear_extrude(1)
|
||||||
|
text("lights", size = 3);
|
||||||
|
|
||||||
|
translate([67, case_width, wall_thickness + 8])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 180, 0])
|
||||||
|
linear_extrude(1)
|
||||||
|
text("left", size = 3);
|
||||||
|
|
||||||
|
translate([55, case_width, wall_thickness + 8])
|
||||||
|
rotate([90, 0, 0])
|
||||||
|
rotate([0, 180, 0])
|
||||||
|
linear_extrude(1)
|
||||||
|
text("right", size = 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
color("green", 1) translate([8.5 + wall_thickness, case_width - wall_thickness - threshold, wall_thickness])
|
||||||
|
cube([60, wall_thickness * 2, 7]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module lid() {
|
||||||
|
lid_width = case_width + hinge_radius * 2 + wall_thickness;
|
||||||
|
union() {
|
||||||
|
difference() {
|
||||||
|
box_face([case_length,
|
||||||
|
lid_width,
|
||||||
|
wall_thickness],
|
||||||
|
bevel);
|
||||||
|
translate([(case_length - 60) / 2, 14 + hinge_radius * 2, -threshold / 2])
|
||||||
|
cube([60, 16, wall_thickness + threshold]);
|
||||||
|
}
|
||||||
|
|
||||||
|
translate([case_length / 4 + 1, hinge_radius - 0.4, -hinge_radius])
|
||||||
|
rotate([180, 0, 0])
|
||||||
|
rotate([0, 90, 0]) hinge(case_length / 2 - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// main_case();
|
||||||
|
// color("red", 1) translate([0, 0, case_height + wall_thickness / 2]) lid();
|
||||||
|
// color("red", 1) translate([0, 0, 40]) lid();
|
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
include <./control_panel.scad>
|
||||||
|
|
||||||
|
main_case();
|
|
@ -0,0 +1,5 @@
|
||||||
|
|
||||||
|
include <./control_panel.scad>
|
||||||
|
|
||||||
|
lid();
|
||||||
|
|
|
@ -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" }
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
];
|
|
@ -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];
|
|
@ -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 = "*" }
|
|
@ -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);
|
||||||
|
}
|
|
@ -35,7 +35,7 @@ macro_rules! define_config {
|
||||||
$($name($struct)),+
|
$($name($struct)),+
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
values: std::collections::HashMap<ConfigName, ConfigOption>,
|
values: std::collections::HashMap<ConfigName, ConfigOption>,
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,519 @@
|
||||||
|
{
|
||||||
|
"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#annotate-snippets@0.9.2": "07p8r6jzb7nqydq0kr5pllckqcdxlyld2g275v425axnzffpxbyc",
|
||||||
|
"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#arrayvec@0.7.4": "04b7n722jij0v3fnm3qk072d5ysc2q30rl9fz33zpfhzah30mlwn",
|
||||||
|
"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@2.3.1": "0rggn074kbqxxajci1aq14b17gp75rw9l6rpbazcv9q0bc6ap5wg",
|
||||||
|
"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.13.0": "059nbiyijwbndyrz0050skvlvzhds0dmnl0biwmxwbw055glfd66",
|
||||||
|
"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#az@1.2.1": "0ww9k1w3al7x5qmb7f13v3s9c2pg1pdxbs8xshqy6zyrchj4qzkv",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#backtrace@0.3.69": "0dsq23dhw4pfndkx2nsa1ml2g31idm7ss7ljxp8d57avygivg290",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#bare-metal@0.2.5": "1cy5pbb92fznnri72y6drfpjxj4qdmd62f0rrlgy70dxlppn9ssx",
|
||||||
|
"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#bindgen@0.69.4": "18194611hn3k1dkxlha7a52sr8vmfhl9blc54xhj08cahd8wh3d0",
|
||||||
|
"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#bitfield@0.13.2": "06g7jb5r2b856vnhx76081fg90jvmy61kjqcfjysgmd5hclvvbs6",
|
||||||
|
"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#cexpr@0.6.0": "0rl77bwhs5p979ih4r0202cn5jrfsrbgrksp40lkfz5vk1x3ib3g",
|
||||||
|
"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#clang-sys@1.8.1": "1x1r9yqss76z8xwpdanw313ss6fniwc1r7dzb5ycjn0ph53kj0hb",
|
||||||
|
"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#convert_case@0.6.0": "1jn1pq6fp3rri88zyw6jlhwwgf6qiyc08d6gjv0qypgkl862n67c",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cookie-factory@0.3.3": "18mka6fk3843qq3jw1fdfvzyv05kx7kcmirfbs2vg2kbw9qzm1cq",
|
||||||
|
"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#cortex-m-rt-macros@0.7.0": "1iyki0wq8pj0qbjhw1mbq5njraihhyr7ydcbqzdzwg10dziz7xph",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cortex-m-rt@0.7.3": "1cfxg502gvcmaczmaij5maxbvaxnda5w6gp14cbin44ksl9yi17f",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cortex-m@0.7.7": "1fbca698v4gv57mv5fc48jrz8wcy6sv675n6fsrsah4qykc11ilf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#cpufeatures@0.2.11": "1l0gzsyy576n017g9bf0vkv5hhg9cpz1h1libxyfdlzcgbh0yhnf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#crc-any@2.5.0": "0wzs26q5cf29fhfnrkrjsr8dpai0rlm4im8b53by8rbrbzzwjbm6",
|
||||||
|
"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#critical-section@1.1.2": "05pj0pvkdyc9r30xxabam4n8zxdbzxcddr0gdypajcbqjgwgynbh",
|
||||||
|
"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#debug-helper@0.3.13": "0bhnpzpgmg8dkdr27g2b49slf6ca79m4idcb01z2krs0qkifhy7m",
|
||||||
|
"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#embedded-alloc@0.5.1": "05gqqv9nyr33vbd0i8ab2bmfcc5kwgk0msk4pk7w5fncba8igbnx",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#embedded-dma@0.2.0": "0ijld5jblcka4b95s1lwxd9k109nyaap34h44g122ddjbidpwkwr",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#embedded-hal@0.2.7": "1zv6pkgg2yl0mzvh3jp326rhryqfnv4l27h78v7p7maag629i51m",
|
||||||
|
"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@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#fixed@1.24.0": "0zbfwzk4mrfbawpx2ahz533bkb97jzihv7fxiyhpmwf0wzkrrih2",
|
||||||
|
"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#frunk@0.4.2": "11v242h7zjka0lckxcffn5pjgr3jzxyljy7ffr0ppy8jkssm38qi",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#frunk_core@0.4.2": "1mjqnn7dclwn8d5g0mrfkg360cgn70a7mm8arx6fc1xxn3x6j95g",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#frunk_derives@0.4.2": "0blsy6aq6rbvxcc0337g15083w24s8539fmv8rwp1qan2qprkymh",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#frunk_proc_macro_helpers@0.1.2": "0b1xl4cfrfai7qi5cb4h9x0967miv3dvwvnsmr1vg4ljhgflmd9m",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fuchsia-cprng@0.1.1": "1fnkqrbz7ixxzsb04bsz9p0zzazanma8znfdqjvh39n14vapfvx0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#fugit@0.3.7": "1rzp49521akq49vs9m8llgmdkk08zb77rry10a7srm9797b6l60p",
|
||||||
|
"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@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#gcd@2.3.0": "06l4fib4dh4m6gazdrzzzinhvcpcfh05r4i4gzscl03vnjhqnx8x",
|
||||||
|
"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#glob@0.3.1": "16zca52nglanv23q5qrwd5jinw3d3as5ylya6y1pbx47vkxvrynj",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#gloo-timers@0.3.0": "1519157n7xppkk6pdw5w52vy1llzn5iljkqd7q1h5609jv7l7cdv",
|
||||||
|
"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.9": "092hxjbjnq5fmz66grd9plxd0sh6ssg5fhgwwwqbrzgzkjwdycfj",
|
||||||
|
"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#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#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.10.5": "0ww45h7nxx5kj6z2y6chlskxd1igvs4j507anr6dzg99x1h25zdh",
|
||||||
|
"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#lazycell@1.3.0": "0m8gw7dn30i0zjjpjdyf6pc16c34nl71lpv461mix50x3p70h3c3",
|
||||||
|
"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#libloading@0.8.5": "194dvczq4sifwkzllfmw0qkgvilpha7m5xy90gd6i446vcpz4ya9",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libm@0.2.8": "0n4hk1rs8pzw8hdfmwn96c4568s93kfxqgcqswr7sajd2diaihjf",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libspa-sys@0.8.0": "07yh4i5grzbxkchg6dnxlwbdw2wm5jnd7ffbhl77jr0388b9f3dz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libspa@0.8.0": "044qs48yl0llp2dmrgwxj9y1pgfy09i6fhq661zqqb9a3fwa9wv5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#libsqlite3-sys@0.27.0": "05pp60ncrmyjlxxjj187808jkvpxm06w5lvvdwwvxd2qrmnj4kng",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#linked_list_allocator@0.10.5": "11k2dv6v5kq45kbvahll434f9iwfw0vsyaycp76q3vh5ahzldyls",
|
||||||
|
"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@1.0.2": "1v1cnnn44awxbcfm4zlavwgkvbyg7gp5zzjm8mqf1apkrwflvq40",
|
||||||
|
"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#nb@0.1.3": "0vyh31pbwrg21f8hz1ipb9i20qwnfwx47gz92i9frdhk0pd327c0",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#nb@1.1.0": "179kbn9l6vhshncycagis7f8mfjppz4fhvgnmcikqz30mp23jm4d",
|
||||||
|
"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#num_enum@0.5.11": "1japmqhcxwn1d3k7q8jw58y7xfby51s16nzd6dkj483cj2pnqr0z",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#num_enum_derive@0.5.11": "16f7r4jila0ckcgdnfgqyhhb90w9m2pdbwayyqmwcci0j6ygkgyw",
|
||||||
|
"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#panic-halt@0.2.0": "04nqaa97ph20ppyy58grwr23hrbw83pn0gf7apf73rdx1q7595ny",
|
||||||
|
"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#pio@0.2.1": "1qvq03nbx6vjix7spr5fcxcbxw39flm1y72kxl1g728gnna9dq3n",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#piper@0.2.1": "1m45fkdq7q5l9mv3b0ra10qwm0kb67rjp2q8y91958gbqjqk33b6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pipewire-sys@0.8.0": "04hiy3rl8v3j2dfzp04gr7r8l5azzqqsvqdzwa7sipdij27ii7l4",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#pipewire@0.8.0": "1nldg1hz4v0qr26lzdxqpvrac4zbc3pb6436sl392425bjx4brh8",
|
||||||
|
"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@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#rp-pico@0.8.0": "0mmx8dyl0q1a9fgz12hrvwd7civqbd1j7g1w5c5i6pcfdwg7fhb3",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rp2040-boot2@0.3.0": "08dv9ndvdzyjz4wdlxcikf1m1s6wwi80027ldkihx59zyr2g74kw",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rp2040-hal-macros@0.1.0": "0piaczzlbrfdhidnqkg04xs1rzal3w3zjplrh6pf3vwpwiir0iw6",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rp2040-hal@0.9.2": "1jk725cf6nx6rhn06swbx47yaq3j134m0hpnv47p5mkdgspbkwhz",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#rp2040-pac@0.5.0": "0k3fm4fww6gcy7w7zwbmmqn9wzz4sih13l1m93sl7x8mb0vxin8j",
|
||||||
|
"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.2.3": "02h3x57lcr8l2pm0a645s9whdh33pn5cnrwvn5cb57vcrc53x3hk",
|
||||||
|
"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.38.28": "05m3vacvbqbg6r6ksmx9k5afpi0lppjdv712crrpsrfax2jp5rbj",
|
||||||
|
"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-parser@0.7.0": "18vhypw6zgccnrlm5ps1pwa0khz7ry927iznpr88b87cagr1v2iq",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#semver@0.9.0": "00q4lkcj0rrgbhviv9sd4p6qmdsipkwkbra7rh11jrhq5kpvjzhx",
|
||||||
|
"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.209": "029yqqbb3c8v3gc720fhxn49dhgvb88zbyprdg5621riwzzy1z4r",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_derive@1.0.209": "0w114ksg1ymnmqdisd0g1j3g8jgz6pam45xg6yb47dfpkybip0x5",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#serde_json@1.0.127": "1b99lgg1d986gwz5fbmmzmvjmqg5bx0lzmhy6rqp5gc2kxnw0hw0",
|
||||||
|
"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#shlex@1.3.0": "0r1y6bv26c1scpxvhg2cabimrmwgbp4p3wy6syj9n0c4s3q2znhg",
|
||||||
|
"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.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#stable_deref_trait@1.2.0": "1lxjr8q2n534b2lhkxd6l6wcddzjvnksi58zv11f9y0jjmr15wd8",
|
||||||
|
"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.4.0": "0lnpg14h1v3fh2jvnc8cz7cjf0m7z1xgkwfpcyy632g829imjgb9",
|
||||||
|
"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.21.0": "0f5wj0crsx74rlll97lhw0wk6y12nhdnqvmnjx002hjn08fmcfy8",
|
||||||
|
"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.39.3": "1xgzhj7bxqqpjaabjkgsx8hi0f600bzj4iyp9f0a9gr3k6dwkawv",
|
||||||
|
"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.21.0": "1qaphb5kgwgid19p64grhv2b9kxy7f1059yy92l9kwrlx90sdwcy",
|
||||||
|
"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-width@0.1.13": "0p92vl8n7qc8mxz45xn6qbgi0259z96n32a158l6vj5bywwdadh3",
|
||||||
|
"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#usb-device@0.2.9": "0205a850jhw9gb96scwfx1k4iwpjvighvz3m80mjkda9r2nw6v0z",
|
||||||
|
"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#vcell@0.1.3": "00n0ss2z3rh0ihig6d4w7xp72g58f7g1m6s5v4h3nc6jacdrqhvp",
|
||||||
|
"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#void@1.0.2": "0zc8f0ksxvmhvgx4fdg0zyn6vdnbxd2xv9hfx4nhzg6kbs4f80ka",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#volatile-register@0.2.2": "1k0rkm81qyhn4r8f03z0sch2kyikkgjjfalpaami9c08c8m7whyy",
|
||||||
|
"registry+https://github.com/rust-lang/crates.io-index#wait-timeout@0.2.0": "1xpkk0j5l9pfmjfh1pi0i89invlavfrd9av5xp0zhxgb29dhy84z",
|
||||||
|
"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.7": "07137zd13lchy5hxpspd0hs6sl19b0fv2zc1chf02nwnzw1d4y23",
|
||||||
|
"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#yansi-term@0.1.2": "1w8vjlvxba6yvidqdvxddx3crl6z66h39qxj8xi6aqayw2nk0p7y",
|
||||||
|
"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"
|
||||||
|
}
|
|
@ -1,12 +1,13 @@
|
||||||
[package]
|
[package]
|
||||||
name = "dashboard"
|
name = "dashboard"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
adw = { version = "0.5", package = "libadwaita", features = [ "v1_2" ] }
|
||||||
|
async-std = { version = "1.13" }
|
||||||
cairo-rs = { version = "0.18" }
|
cairo-rs = { version = "0.18" }
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
chrono = { version = "0.4", features = ["serde"] }
|
||||||
fluent-ergonomics = { path = "../fluent-ergonomics/" }
|
fluent-ergonomics = { path = "../fluent-ergonomics/" }
|
||||||
|
@ -17,13 +18,11 @@ gio = { version = "0.18" }
|
||||||
glib = { version = "0.18" }
|
glib = { version = "0.18" }
|
||||||
gdk = { version = "0.7", package = "gdk4" }
|
gdk = { version = "0.7", package = "gdk4" }
|
||||||
gtk = { version = "0.7", package = "gtk4" }
|
gtk = { version = "0.7", package = "gtk4" }
|
||||||
ifc = { path = "../ifc/" }
|
|
||||||
lazy_static = { version = "1.4" }
|
lazy_static = { version = "1.4" }
|
||||||
memorycache = { path = "../memorycache/" }
|
memorycache = { path = "../memorycache/" }
|
||||||
reqwest = { version = "0.11", features = ["json"] }
|
reqwest = { version = "0.11", features = ["json"] }
|
||||||
serde_derive = { version = "1" }
|
|
||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
serde = { version = "1" }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
unic-langid = { version = "0.9" }
|
unic-langid = { version = "0.9" }
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,10 @@ impl ApplicationWindow {
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
let date_label = Date::default();
|
let date_label = Date::default();
|
||||||
layout.append(&date_label);
|
let header = adw::HeaderBar::builder()
|
||||||
|
.title_widget(&date_label)
|
||||||
|
.build();
|
||||||
|
layout.append(&header);
|
||||||
|
|
||||||
let events = Events::default();
|
let events = Events::default();
|
||||||
layout.append(&events);
|
layout.append(&events);
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
use chrono::Datelike;
|
|
||||||
use glib::Object;
|
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
|
||||||
use ifc::IFC;
|
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
use chrono::NaiveDate;
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
|
|
||||||
pub struct DatePrivate {
|
pub struct DatePrivate {
|
||||||
date: Rc<RefCell<IFC>>,
|
date: Rc<RefCell<NaiveDate>>,
|
||||||
label: Rc<RefCell<gtk::Label>>,
|
label: Rc<RefCell<gtk::Label>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DatePrivate {
|
impl Default for DatePrivate {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let date = chrono::Local::now().date_naive();
|
let date = chrono::Local::now().date_naive();
|
||||||
let year = date.year();
|
|
||||||
let date = date.with_year(year + 10000).unwrap();
|
|
||||||
Self {
|
Self {
|
||||||
date: Rc::new(RefCell::new(IFC::from(date))),
|
date: Rc::new(RefCell::new(date)),
|
||||||
label: Rc::new(RefCell::new(gtk::Label::new(None))),
|
label: Rc::new(RefCell::new(gtk::Label::new(None))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,19 +50,16 @@ impl Default for Date {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Date {
|
impl Date {
|
||||||
pub fn update_date(&self, date: IFC) {
|
pub fn update_date(&self, date: NaiveDate) {
|
||||||
*self.imp().date.borrow_mut() = date;
|
*self.imp().date.borrow_mut() = date;
|
||||||
self.redraw();
|
self.redraw();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redraw(&self) {
|
fn redraw(&self) {
|
||||||
let date = self.imp().date.borrow().clone();
|
let date = self.imp().date.borrow();
|
||||||
self.imp().label.borrow_mut().set_text(&format!(
|
self.imp()
|
||||||
"{:?}, {:?} {}, {}",
|
.label
|
||||||
date.weekday(),
|
.borrow_mut()
|
||||||
date.month(),
|
.set_text(&date.format("%Y %B %d").to_string());
|
||||||
date.day(),
|
|
||||||
date.year()
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ use crate::{
|
||||||
};
|
};
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
use ifc::IFC;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
|
@ -59,19 +58,19 @@ impl Events {
|
||||||
pub fn set_events(&self, events: YearlyEvents, next_event: solstices::Event) {
|
pub fn set_events(&self, events: YearlyEvents, next_event: solstices::Event) {
|
||||||
self.imp()
|
self.imp()
|
||||||
.spring_equinox
|
.spring_equinox
|
||||||
.update_date(IFC::from(events.spring_equinox.date_naive()));
|
.update_date(events.spring_equinox.date_naive());
|
||||||
|
|
||||||
self.imp()
|
self.imp()
|
||||||
.summer_solstice
|
.summer_solstice
|
||||||
.update_date(IFC::from(events.summer_solstice.date_naive()));
|
.update_date(events.summer_solstice.date_naive());
|
||||||
|
|
||||||
self.imp()
|
self.imp()
|
||||||
.autumn_equinox
|
.autumn_equinox
|
||||||
.update_date(IFC::from(events.autumn_equinox.date_naive()));
|
.update_date(events.autumn_equinox.date_naive());
|
||||||
|
|
||||||
self.imp()
|
self.imp()
|
||||||
.winter_solstice
|
.winter_solstice
|
||||||
.update_date(IFC::from(events.winter_solstice.date_naive()));
|
.update_date(events.winter_solstice.date_naive());
|
||||||
|
|
||||||
self.imp().spring_equinox.remove_css_class("highlight");
|
self.imp().spring_equinox.remove_css_class("highlight");
|
||||||
self.imp().summer_solstice.remove_css_class("highlight");
|
self.imp().summer_solstice.remove_css_class("highlight");
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
use chrono::{Datelike, Local, Utc};
|
|
||||||
use geo_types::{Latitude, Longitude};
|
|
||||||
use glib::Sender;
|
|
||||||
use gtk::prelude::*;
|
|
||||||
use ifc::IFC;
|
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use async_std::channel::Sender;
|
||||||
|
use chrono::{Datelike, Local, Utc};
|
||||||
|
use geo_types::{Latitude, Longitude};
|
||||||
|
use gtk::prelude::*;
|
||||||
|
|
||||||
mod app_window;
|
mod app_window;
|
||||||
use app_window::ApplicationWindow;
|
use app_window::ApplicationWindow;
|
||||||
|
|
||||||
|
@ -102,14 +102,17 @@ pub fn main() {
|
||||||
|
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
let state = State {
|
let state = State {
|
||||||
date: IFC::from(now.date_naive().with_year(now.year() + 10000).unwrap()),
|
date: now.date_naive(),
|
||||||
next_event: EVENTS.next_event(now.with_timezone(&Utc)).unwrap(),
|
next_event: EVENTS.next_event(now.with_timezone(&Utc)).unwrap(),
|
||||||
events: EVENTS.yearly_events(now.year()).unwrap(),
|
events: EVENTS.yearly_events(now.year()).unwrap(),
|
||||||
transit: Some(transit),
|
transit: Some(transit),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(ref gtk_tx) = *core.tx.read().unwrap() {
|
let gtk_tx = core.tx.read().unwrap().clone();
|
||||||
let _ = gtk_tx.send(Message::Refresh(state.clone()));
|
|
||||||
|
if let Some(gtk_tx) = gtk_tx {
|
||||||
|
let state = state.clone();
|
||||||
|
let _ = gtk_tx.send(Message::Refresh(state)).await;
|
||||||
std::thread::sleep(std::time::Duration::from_secs(60));
|
std::thread::sleep(std::time::Duration::from_secs(60));
|
||||||
} else {
|
} else {
|
||||||
std::thread::sleep(std::time::Duration::from_secs(1));
|
std::thread::sleep(std::time::Duration::from_secs(1));
|
||||||
|
@ -119,21 +122,17 @@ pub fn main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
app.connect_activate(move |app| {
|
app.connect_activate(move |app| {
|
||||||
let (gtk_tx, gtk_rx) =
|
let (gtk_tx, gtk_rx) = async_std::channel::unbounded();
|
||||||
gtk::glib::MainContext::channel::<Message>(gtk::glib::Priority::DEFAULT);
|
|
||||||
|
|
||||||
*core.tx.write().unwrap() = Some(gtk_tx);
|
*core.tx.write().unwrap() = Some(gtk_tx);
|
||||||
|
|
||||||
let window = ApplicationWindow::new(app);
|
let window = ApplicationWindow::new(app);
|
||||||
window.window.present();
|
window.window.present();
|
||||||
|
|
||||||
gtk_rx.attach(None, {
|
glib::spawn_future_local(async move {
|
||||||
let window = window.clone();
|
loop {
|
||||||
move |msg| {
|
let Message::Refresh(state) = gtk_rx.recv().await.unwrap();
|
||||||
let Message::Refresh(state) = msg;
|
window.update_state(state);
|
||||||
ApplicationWindow::update_state(&window, state);
|
|
||||||
|
|
||||||
glib::ControlFlow::Continue
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
// http://astropixels.com/ephemeris/soleq2001.html
|
// http://astropixels.com/ephemeris/soleq2001.html
|
||||||
const SOLSTICE_TEXT: &str = "
|
const SOLSTICE_TEXT: &str = "
|
||||||
|
|
|
@ -2,11 +2,11 @@ use crate::{
|
||||||
solstices::{Event, YearlyEvents},
|
solstices::{Event, YearlyEvents},
|
||||||
soluna_client::SunMoon,
|
soluna_client::SunMoon,
|
||||||
};
|
};
|
||||||
use ifc::IFC;
|
use chrono::NaiveDate;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct State {
|
pub struct State {
|
||||||
pub date: IFC,
|
pub date: NaiveDate,
|
||||||
pub next_event: Event,
|
pub next_event: Event,
|
||||||
pub events: YearlyEvents,
|
pub events: YearlyEvents,
|
||||||
pub transit: Option<SunMoon>,
|
pub transit: Option<SunMoon>,
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
pkgs.udev
|
pkgs.udev
|
||||||
pkgs.wasm-pack
|
pkgs.wasm-pack
|
||||||
typeshare.packages."x86_64-linux".default
|
typeshare.packages."x86_64-linux".default
|
||||||
|
pkgs.nodePackages_latest.typescript-language-server
|
||||||
];
|
];
|
||||||
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
|
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
|
||||||
ENV = "dev";
|
ENV = "dev";
|
||||||
|
@ -70,7 +71,7 @@
|
||||||
|
|
||||||
dashboard = attrs: { nativeBuildInputs = gtkNativeInputs; };
|
dashboard = attrs: { nativeBuildInputs = gtkNativeInputs; };
|
||||||
fitnesstrax = import ./fitnesstrax/app/override.nix { gtkNativeInputs = gtkNativeInputs; };
|
fitnesstrax = import ./fitnesstrax/app/override.nix { gtkNativeInputs = gtkNativeInputs; };
|
||||||
kifu-gtk = import ./kifu/gtk/override.nix { gtkNativeInputs = gtkNativeInputs; };
|
otg-gtk = import ./otg/gtk/override.nix { gtkNativeInputs = gtkNativeInputs; };
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -84,7 +85,7 @@
|
||||||
dashboard = cargo_nix.workspaceMembers.dashboard.build;
|
dashboard = cargo_nix.workspaceMembers.dashboard.build;
|
||||||
file-service = cargo_nix.workspaceMembers.file-service.build;
|
file-service = cargo_nix.workspaceMembers.file-service.build;
|
||||||
fitnesstrax = cargo_nix.workspaceMembers.fitnesstrax.build;
|
fitnesstrax = cargo_nix.workspaceMembers.fitnesstrax.build;
|
||||||
kifu-gtk = cargo_nix.workspaceMembers.kifu-gtk.build;
|
otg-gtk = cargo_nix.workspaceMembers.otg-gtk.build;
|
||||||
|
|
||||||
all = pkgs.symlinkJoin {
|
all = pkgs.symlinkJoin {
|
||||||
name = "all";
|
name = "all";
|
||||||
|
@ -93,7 +94,7 @@
|
||||||
dashboard
|
dashboard
|
||||||
file-service
|
file-service
|
||||||
fitnesstrax
|
fitnesstrax
|
||||||
kifu-gtk
|
otg-gtk
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
@ -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(())
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
|
@ -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*
|
|
@ -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 can’t go back!**
|
||||||
|
|
||||||
|
If you aren’t 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 you’re on your own.
|
||||||
|
|
||||||
|
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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/).
|
File diff suppressed because it is too large
Load Diff
|
@ -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 |
|
@ -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 |
|
@ -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"
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
|
@ -0,0 +1,5 @@
|
||||||
|
.layout {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -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();
|
||||||
|
});
|
|
@ -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;
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -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 {
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
}
|
|
@ -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();
|
|
@ -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 |
|
@ -0,0 +1 @@
|
||||||
|
/// <reference types="react-scripts" />
|
|
@ -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;
|
|
@ -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';
|
|
@ -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"
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,591 +0,0 @@
|
||||||
# This file is automatically @generated by Cargo.
|
|
||||||
# It is not intended for manual editing.
|
|
||||||
version = 3
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "android_system_properties"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "autocfg"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bumpalo"
|
|
||||||
version = "3.12.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cc"
|
|
||||||
version = "1.0.79"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-if"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chrono"
|
|
||||||
version = "0.4.24"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
|
|
||||||
dependencies = [
|
|
||||||
"iana-time-zone",
|
|
||||||
"js-sys",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
"serde",
|
|
||||||
"time",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "codespan-reporting"
|
|
||||||
version = "0.11.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
|
|
||||||
dependencies = [
|
|
||||||
"termcolor",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cool_asserts"
|
|
||||||
version = "2.0.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ee9f254e53f61e2688d3677fa2cbe4e9b950afd56f48819c98817417cf6b28ec"
|
|
||||||
dependencies = [
|
|
||||||
"indent_write",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "core-foundation-sys"
|
|
||||||
version = "0.8.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cxx"
|
|
||||||
version = "1.0.94"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f61f1b6389c3fe1c316bf8a4dccc90a38208354b330925bce1f74a6c4756eb93"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"cxxbridge-flags",
|
|
||||||
"cxxbridge-macro",
|
|
||||||
"link-cplusplus",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cxx-build"
|
|
||||||
version = "1.0.94"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "12cee708e8962df2aeb38f594aae5d827c022b6460ac71a7a3e2c3c2aae5a07b"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"codespan-reporting",
|
|
||||||
"once_cell",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"scratch",
|
|
||||||
"syn 2.0.12",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cxxbridge-flags"
|
|
||||||
version = "1.0.94"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7944172ae7e4068c533afbb984114a56c46e9ccddda550499caa222902c7f7bb"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cxxbridge-macro"
|
|
||||||
version = "1.0.94"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2345488264226bf682893e25de0769f3360aac9957980ec49361b083ddaa5bc5"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.12",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "grid"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0634107a3a005070dd73e27e74ecb691a94e9e5ba7829f434db7fbf73a6b5c47"
|
|
||||||
dependencies = [
|
|
||||||
"no-std-compat",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "iana-time-zone"
|
|
||||||
version = "0.1.56"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
|
|
||||||
dependencies = [
|
|
||||||
"android_system_properties",
|
|
||||||
"core-foundation-sys",
|
|
||||||
"iana-time-zone-haiku",
|
|
||||||
"js-sys",
|
|
||||||
"wasm-bindgen",
|
|
||||||
"windows",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "iana-time-zone-haiku"
|
|
||||||
version = "0.1.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca"
|
|
||||||
dependencies = [
|
|
||||||
"cxx",
|
|
||||||
"cxx-build",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "indent_write"
|
|
||||||
version = "2.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0cfe9645a18782869361d9c8732246be7b410ad4e919d3609ebabdac00ba12c3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "itoa"
|
|
||||||
version = "1.0.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "js-sys"
|
|
||||||
version = "0.3.61"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730"
|
|
||||||
dependencies = [
|
|
||||||
"wasm-bindgen",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "kifu-core"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"cool_asserts",
|
|
||||||
"grid",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"sgf",
|
|
||||||
"thiserror",
|
|
||||||
"typeshare",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libc"
|
|
||||||
version = "0.2.140"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "link-cplusplus"
|
|
||||||
version = "1.0.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "log"
|
|
||||||
version = "0.4.17"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "memchr"
|
|
||||||
version = "2.5.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "minimal-lexical"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "no-std-compat"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "nom"
|
|
||||||
version = "7.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
|
||||||
dependencies = [
|
|
||||||
"memchr",
|
|
||||||
"minimal-lexical",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-integer"
|
|
||||||
version = "0.1.45"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"num-traits",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-traits"
|
|
||||||
version = "0.2.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "once_cell"
|
|
||||||
version = "1.17.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "1.0.52"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "1.0.26"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ryu"
|
|
||||||
version = "1.0.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scratch"
|
|
||||||
version = "1.0.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde"
|
|
||||||
version = "1.0.162"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "71b2f6e1ab5c2b98c05f0f35b236b22e8df7ead6ffbf51d7808da7f8817e7ab6"
|
|
||||||
dependencies = [
|
|
||||||
"serde_derive",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_derive"
|
|
||||||
version = "1.0.162"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a2a0814352fd64b58489904a44ea8d90cb1a91dcb6b4f5ebabc32c8318e93cb6"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.12",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "serde_json"
|
|
||||||
version = "1.0.96"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1"
|
|
||||||
dependencies = [
|
|
||||||
"itoa",
|
|
||||||
"ryu",
|
|
||||||
"serde",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sgf"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"nom",
|
|
||||||
"serde",
|
|
||||||
"thiserror",
|
|
||||||
"typeshare",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "1.0.109"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "syn"
|
|
||||||
version = "2.0.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"unicode-ident",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "termcolor"
|
|
||||||
version = "1.2.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
|
|
||||||
dependencies = [
|
|
||||||
"winapi-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror"
|
|
||||||
version = "1.0.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
|
|
||||||
dependencies = [
|
|
||||||
"thiserror-impl",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "thiserror-impl"
|
|
||||||
version = "1.0.40"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.12",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time"
|
|
||||||
version = "0.1.45"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"wasi",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typeshare"
|
|
||||||
version = "1.0.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f44d1a2f454cb35fbe05b218c410792697e76bd868f48d3a418f2cd1a7d527d6"
|
|
||||||
dependencies = [
|
|
||||||
"chrono",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"typeshare-annotation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typeshare-annotation"
|
|
||||||
version = "1.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fc670d0e358428857cc3b4bf504c691e572fccaec9542ff09212d3f13d74b7a9"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-ident"
|
|
||||||
version = "1.0.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-width"
|
|
||||||
version = "0.1.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasi"
|
|
||||||
version = "0.10.0+wasi-snapshot-preview1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen"
|
|
||||||
version = "0.2.84"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"wasm-bindgen-macro",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-backend"
|
|
||||||
version = "0.2.84"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9"
|
|
||||||
dependencies = [
|
|
||||||
"bumpalo",
|
|
||||||
"log",
|
|
||||||
"once_cell",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
"wasm-bindgen-shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-macro"
|
|
||||||
version = "0.2.84"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5"
|
|
||||||
dependencies = [
|
|
||||||
"quote",
|
|
||||||
"wasm-bindgen-macro-support",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-macro-support"
|
|
||||||
version = "0.2.84"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 1.0.109",
|
|
||||||
"wasm-bindgen-backend",
|
|
||||||
"wasm-bindgen-shared",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasm-bindgen-shared"
|
|
||||||
version = "0.2.84"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi"
|
|
||||||
version = "0.3.9"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
|
||||||
dependencies = [
|
|
||||||
"winapi-i686-pc-windows-gnu",
|
|
||||||
"winapi-x86_64-pc-windows-gnu",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-i686-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-util"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
|
||||||
dependencies = [
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
|
||||||
version = "0.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
|
|
||||||
dependencies = [
|
|
||||||
"windows-targets",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows-targets"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
|
|
||||||
dependencies = [
|
|
||||||
"windows_aarch64_gnullvm",
|
|
||||||
"windows_aarch64_msvc",
|
|
||||||
"windows_i686_gnu",
|
|
||||||
"windows_i686_msvc",
|
|
||||||
"windows_x86_64_gnu",
|
|
||||||
"windows_x86_64_gnullvm",
|
|
||||||
"windows_x86_64_msvc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_gnullvm"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_aarch64_msvc"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_gnu"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_i686_msvc"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnu"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_gnullvm"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "windows_x86_64_msvc"
|
|
||||||
version = "0.48.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
|
|
@ -1,17 +0,0 @@
|
||||||
extern crate config_derive;
|
|
||||||
|
|
||||||
mod api;
|
|
||||||
pub use api::{
|
|
||||||
ChangeSettingRequest, Core, CoreNotification, CoreRequest, CoreResponse, CreateGameRequest,
|
|
||||||
HotseatPlayerRequest, Observable, PlayerInfoRequest,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod board;
|
|
||||||
pub use board::*;
|
|
||||||
|
|
||||||
mod database;
|
|
||||||
|
|
||||||
mod types;
|
|
||||||
pub use types::{BoardError, Color, Config, ConfigOption, DatabasePath, Player, Rank, Size};
|
|
||||||
|
|
||||||
pub mod ui;
|
|
|
@ -1,240 +0,0 @@
|
||||||
use crate::{
|
|
||||||
api::PlayStoneRequest,
|
|
||||||
board::{Coordinate, Goban},
|
|
||||||
database::Database,
|
|
||||||
};
|
|
||||||
use config::define_config;
|
|
||||||
use config_derive::ConfigOption;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::{path::PathBuf, time::Duration};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
define_config! {
|
|
||||||
DatabasePath(DatabasePath),
|
|
||||||
Me(Me),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
|
|
||||||
pub struct DatabasePath(pub PathBuf);
|
|
||||||
|
|
||||||
impl std::ops::Deref for DatabasePath {
|
|
||||||
type Target = PathBuf;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<String> for DatabasePath {
|
|
||||||
fn from(s: String) -> Self {
|
|
||||||
Self(PathBuf::from(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
|
|
||||||
pub struct Me(Player);
|
|
||||||
|
|
||||||
impl std::ops::Deref for Me {
|
|
||||||
type Target = Player;
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Error)]
|
|
||||||
pub enum BoardError {
|
|
||||||
#[error("Position is invalid")]
|
|
||||||
InvalidPosition,
|
|
||||||
#[error("Self-capture is forbidden")]
|
|
||||||
SelfCapture,
|
|
||||||
#[error("Ko")]
|
|
||||||
Ko,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum Color {
|
|
||||||
Black,
|
|
||||||
White,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub struct Size {
|
|
||||||
pub width: u8,
|
|
||||||
pub height: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Size {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
width: 19,
|
|
||||||
height: 19,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct AppState {
|
|
||||||
pub game: Option<GameState>,
|
|
||||||
pub database: Database,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppState {
|
|
||||||
pub fn new(database_path: DatabasePath) -> Self {
|
|
||||||
Self {
|
|
||||||
game: Some(GameState::default()),
|
|
||||||
database: Database::open_path(database_path.to_path_buf()).unwrap(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn place_stone(&mut self, req: PlayStoneRequest) {
|
|
||||||
if let Some(ref mut game) = self.game {
|
|
||||||
let _ = game.place_stone(Coordinate {
|
|
||||||
column: req.column,
|
|
||||||
row: req.row,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
|
||||||
pub enum Rank {
|
|
||||||
Kyu(u8),
|
|
||||||
Dan(u8),
|
|
||||||
Pro(u8),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&str> for Rank {
|
|
||||||
type Error = String;
|
|
||||||
|
|
||||||
fn try_from(_: &str) -> Result<Rank, Self::Error> {
|
|
||||||
Ok(Rank::Kyu(15))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Rank> for String {
|
|
||||||
fn from(r: Rank) -> String {
|
|
||||||
match r {
|
|
||||||
Rank::Kyu(v) => format!("{} kyu", v),
|
|
||||||
Rank::Dan(v) => format!("{} dan", v),
|
|
||||||
Rank::Pro(v) => format!("{} pro", v),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
||||||
pub struct Player {
|
|
||||||
pub name: String,
|
|
||||||
pub rank: Option<Rank>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct GameState {
|
|
||||||
pub board: Goban,
|
|
||||||
pub past_positions: Vec<Goban>,
|
|
||||||
|
|
||||||
pub conversation: Vec<String>,
|
|
||||||
pub current_player: Color,
|
|
||||||
|
|
||||||
pub white_player: Player,
|
|
||||||
pub black_player: Player,
|
|
||||||
|
|
||||||
pub white_clock: Duration,
|
|
||||||
pub black_clock: Duration,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for GameState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
board: Goban::new(),
|
|
||||||
past_positions: vec![],
|
|
||||||
conversation: vec![],
|
|
||||||
current_player: Color::Black,
|
|
||||||
white_player: Player {
|
|
||||||
name: "".to_owned(),
|
|
||||||
rank: None,
|
|
||||||
},
|
|
||||||
black_player: Player {
|
|
||||||
name: "".to_owned(),
|
|
||||||
rank: None,
|
|
||||||
},
|
|
||||||
white_clock: Duration::from_secs(600),
|
|
||||||
black_clock: Duration::from_secs(600),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GameState {
|
|
||||||
fn place_stone(&mut self, coordinate: Coordinate) -> Result<(), BoardError> {
|
|
||||||
let board = self.board.clone();
|
|
||||||
let new_board = board.place_stone(coordinate, self.current_player)?;
|
|
||||||
|
|
||||||
if self.past_positions.contains(&new_board) {
|
|
||||||
return Err(BoardError::Ko);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.past_positions.push(self.board.clone());
|
|
||||||
self.board = new_board;
|
|
||||||
match self.current_player {
|
|
||||||
Color::White => self.current_player = Color::Black,
|
|
||||||
Color::Black => self.current_player = Color::White,
|
|
||||||
};
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn current_player_changes_after_move() {
|
|
||||||
let mut state = GameState::default();
|
|
||||||
assert_eq!(state.current_player, Color::Black);
|
|
||||||
state.place_stone(Coordinate { column: 9, row: 9 }).unwrap();
|
|
||||||
assert_eq!(state.current_player, Color::White);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn current_player_remains_the_same_after_self_capture() {
|
|
||||||
let mut state = GameState::default();
|
|
||||||
state.board = Goban::from_coordinates(
|
|
||||||
vec![
|
|
||||||
(Coordinate { column: 17, row: 0 }, Color::White),
|
|
||||||
(Coordinate { column: 17, row: 1 }, Color::White),
|
|
||||||
(Coordinate { column: 18, row: 1 }, Color::White),
|
|
||||||
]
|
|
||||||
.into_iter(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
state.current_player = Color::Black;
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
state.place_stone(Coordinate { column: 18, row: 0 }),
|
|
||||||
Err(BoardError::SelfCapture)
|
|
||||||
);
|
|
||||||
assert_eq!(state.current_player, Color::Black);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn ko_rules_are_enforced() {
|
|
||||||
let mut state = GameState::default();
|
|
||||||
state.board = Goban::from_coordinates(
|
|
||||||
vec![
|
|
||||||
(Coordinate { column: 7, row: 9 }, Color::White),
|
|
||||||
(Coordinate { column: 8, row: 8 }, Color::White),
|
|
||||||
(Coordinate { column: 8, row: 10 }, Color::White),
|
|
||||||
(Coordinate { column: 9, row: 9 }, Color::White),
|
|
||||||
(Coordinate { column: 10, row: 9 }, Color::Black),
|
|
||||||
(Coordinate { column: 9, row: 8 }, Color::Black),
|
|
||||||
(Coordinate { column: 9, row: 10 }, Color::Black),
|
|
||||||
]
|
|
||||||
.into_iter(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
state.place_stone(Coordinate { column: 8, row: 9 }).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
state.place_stone(Coordinate { column: 9, row: 9 }),
|
|
||||||
Err(BoardError::Ko)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
use crate::{
|
|
||||||
types::{Config, DatabasePath},
|
|
||||||
ui::Field,
|
|
||||||
};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
|
||||||
pub struct ConfigurationView {
|
|
||||||
pub library: Field<()>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn configuration(config: &Config) -> ConfigurationView {
|
|
||||||
let path: Option<DatabasePath> = config.get();
|
|
||||||
ConfigurationView {
|
|
||||||
library: Field {
|
|
||||||
id: "library-path-field".to_owned(),
|
|
||||||
label: "Library".to_owned(),
|
|
||||||
value: path.map(|path| path.to_string_lossy().into_owned()),
|
|
||||||
action: (),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,3 +0,0 @@
|
||||||
.content {
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 350 KiB |
|
@ -1,85 +0,0 @@
|
||||||
pub mod ui;
|
|
||||||
|
|
||||||
mod view_models;
|
|
||||||
mod views;
|
|
||||||
|
|
||||||
use async_std::task::yield_now;
|
|
||||||
use kifu_core::{Core, CoreRequest, CoreResponse, Observable};
|
|
||||||
use std::{rc::Rc, sync::Arc};
|
|
||||||
use tokio::runtime::Runtime;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct CoreApi {
|
|
||||||
pub rt: Arc<Runtime>,
|
|
||||||
pub core: Core,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CoreApi {
|
|
||||||
pub fn dispatch(&self, request: CoreRequest) {
|
|
||||||
/*
|
|
||||||
spawn({
|
|
||||||
/*
|
|
||||||
let gtk_tx = self.gtk_tx.clone();
|
|
||||||
let core = self.core.clone();
|
|
||||||
async move { gtk_tx.send(core.dispatch(request).await) }
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn perftrace<F, A>(trace_name: &str, f: F) -> A
|
|
||||||
where
|
|
||||||
F: FnOnce() -> A,
|
|
||||||
{
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let result = f();
|
|
||||||
let end = std::time::Instant::now();
|
|
||||||
println!("[Trace: {}] {:?}", trace_name, end - start);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// LocalObserver creates a task on the current thread which watches the specified observer for notifications and calls the handler function with each one.
|
|
||||||
///
|
|
||||||
/// The LocalObserver starts a task which listens for notifications during the constructor. When the observer goes out of scope, it will make a point of aborting the task. This combination means that anything which uses the observer can create it, hold on to a reference of it, and then drop it when done, and not have to do anything else with the observer object.
|
|
||||||
struct LocalObserver<T> {
|
|
||||||
join_handle: glib::JoinHandle<()>,
|
|
||||||
handler: Rc<dyn Fn(T)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: 'static> LocalObserver<T> {
|
|
||||||
/// Construct a new LocalObserver and start it running.
|
|
||||||
///
|
|
||||||
/// observable -- any object which emits events
|
|
||||||
/// handler -- a function which can process events
|
|
||||||
fn new(observable: &dyn Observable<T>, handler: impl Fn(T) + 'static) -> Self {
|
|
||||||
let listener = observable.subscribe();
|
|
||||||
let handler = Rc::new(handler);
|
|
||||||
let join_handle = glib::spawn_future_local({
|
|
||||||
let handler = handler.clone();
|
|
||||||
async move {
|
|
||||||
loop {
|
|
||||||
match listener.recv().await {
|
|
||||||
Ok(msg) => handler(msg),
|
|
||||||
Err(_) => {
|
|
||||||
// recv only fails if the channel has been closed and no other notifications are pending. This will break out of the loop and terminate the observer.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
yield_now().await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Self {
|
|
||||||
join_handle,
|
|
||||||
handler,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Drop for LocalObserver<T> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
// Abort the task when the observer goes out of scope.
|
|
||||||
self.join_handle.abort();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,145 +0,0 @@
|
||||||
use adw::prelude::*;
|
|
||||||
use kifu_core::{Config, ConfigOption, Core, CoreRequest, CoreResponse, DatabasePath};
|
|
||||||
use kifu_gtk::{
|
|
||||||
perftrace,
|
|
||||||
ui::{AppWindow, ConfigurationPage, Home, PlayingField},
|
|
||||||
CoreApi,
|
|
||||||
};
|
|
||||||
use std::sync::{Arc, RwLock};
|
|
||||||
|
|
||||||
const APP_ID_DEV: &str = "com.luminescent-dreams.kifu-gtk.dev";
|
|
||||||
const APP_ID_PROD: &str = "com.luminescent-dreams.kifu-gtk";
|
|
||||||
|
|
||||||
const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/kifu-gtk/";
|
|
||||||
|
|
||||||
fn handle_response(api: CoreApi, app_window: &AppWindow, message: CoreResponse) {
|
|
||||||
let playing_field = Arc::new(RwLock::new(None));
|
|
||||||
match message {
|
|
||||||
CoreResponse::ConfigurationView(view) => perftrace("ConfigurationView", || {
|
|
||||||
let config_page = ConfigurationPage::new(api, view);
|
|
||||||
|
|
||||||
let window = adw::PreferencesWindow::new();
|
|
||||||
window.add(&config_page);
|
|
||||||
window.set_visible_page(&config_page);
|
|
||||||
window.present();
|
|
||||||
}),
|
|
||||||
CoreResponse::HomeView(view) => perftrace("HomeView", || {
|
|
||||||
let api = api.clone();
|
|
||||||
|
|
||||||
let home = Home::new(api, view);
|
|
||||||
app_window.set_content(&home);
|
|
||||||
}),
|
|
||||||
CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || {
|
|
||||||
let api = api.clone();
|
|
||||||
|
|
||||||
let mut playing_field = playing_field.write().unwrap();
|
|
||||||
if playing_field.is_none() {
|
|
||||||
perftrace("creating a new playing field", || {
|
|
||||||
let field = PlayingField::new(api, view);
|
|
||||||
app_window.set_content(&field);
|
|
||||||
*playing_field = Some(field);
|
|
||||||
})
|
|
||||||
} else if let Some(field) = playing_field.as_ref() {
|
|
||||||
field.update_view(view)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
CoreResponse::UpdatedConfigurationView(view) => perftrace("UpdatedConfiguration", || {
|
|
||||||
println!("updated configuration: {:?}", view);
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
gio::resources_register_include!("com.luminescent-dreams.kifu-gtk.gresource")
|
|
||||||
.expect("Failed to register resources");
|
|
||||||
|
|
||||||
let app_id = if std::env::var_os("ENV") == Some("dev".into()) {
|
|
||||||
APP_ID_DEV
|
|
||||||
} else {
|
|
||||||
APP_ID_PROD
|
|
||||||
};
|
|
||||||
|
|
||||||
let settings = gio::Settings::new(app_id);
|
|
||||||
let db_path: String = settings.string("database-path").into();
|
|
||||||
let mut config = Config::new();
|
|
||||||
config.set(ConfigOption::DatabasePath(db_path.into()));
|
|
||||||
|
|
||||||
let runtime = Arc::new(
|
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
|
||||||
.enable_all()
|
|
||||||
.build()
|
|
||||||
.unwrap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
/*
|
|
||||||
let config_path = std::env::var("CONFIG")
|
|
||||||
.map(std::path::PathBuf::from)
|
|
||||||
.or({
|
|
||||||
std::env::var("HOME").map(|base| {
|
|
||||||
let mut config_path = std::path::PathBuf::from(base);
|
|
||||||
config_path.push(".config");
|
|
||||||
config_path.push("kifu");
|
|
||||||
config_path
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.expect("no config path could be found");
|
|
||||||
*/
|
|
||||||
|
|
||||||
let core = Core::new(config);
|
|
||||||
|
|
||||||
/*
|
|
||||||
let core_handle = runtime.spawn({
|
|
||||||
let core = core.clone();
|
|
||||||
async move {
|
|
||||||
core.run().await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
let app = adw::Application::builder()
|
|
||||||
.application_id("com.luminescent-dreams.kifu-gtk")
|
|
||||||
.resource_base_path("/com/luminescent-dreams/kifu-gtk")
|
|
||||||
.build();
|
|
||||||
|
|
||||||
app.connect_activate({
|
|
||||||
let runtime = runtime.clone();
|
|
||||||
move |app| {
|
|
||||||
let app_window = AppWindow::new(app);
|
|
||||||
|
|
||||||
let api = CoreApi {
|
|
||||||
rt: runtime.clone(),
|
|
||||||
core: core.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let action_config = gio::SimpleAction::new("show-config", None);
|
|
||||||
action_config.connect_activate({
|
|
||||||
let api = api.clone();
|
|
||||||
move |_, _| {
|
|
||||||
api.dispatch(CoreRequest::OpenConfiguration);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
app.add_action(&action_config);
|
|
||||||
|
|
||||||
app_window.window.present();
|
|
||||||
|
|
||||||
/*
|
|
||||||
gtk_rx.attach(None, {
|
|
||||||
let api = api.clone();
|
|
||||||
move |message| {
|
|
||||||
perftrace("handle_response", || {
|
|
||||||
handle_response(api.clone(), &app_window, message)
|
|
||||||
});
|
|
||||||
glib::ControlFlow::Continue
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
api.dispatch(CoreRequest::Home);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
println!("running the gtk loop");
|
|
||||||
app.run();
|
|
||||||
|
|
||||||
/* let _ = runtime.block_on(core_handle); */
|
|
||||||
}
|
|
|
@ -1,100 +0,0 @@
|
||||||
use adw::prelude::*;
|
|
||||||
use gio::resources_lookup_data;
|
|
||||||
use glib::IsA;
|
|
||||||
use gtk::STYLE_PROVIDER_PRIORITY_USER;
|
|
||||||
|
|
||||||
mod chat;
|
|
||||||
pub use chat::Chat;
|
|
||||||
|
|
||||||
mod config;
|
|
||||||
pub use config::ConfigurationPage;
|
|
||||||
|
|
||||||
mod game_preview;
|
|
||||||
pub use game_preview::GamePreview;
|
|
||||||
|
|
||||||
mod library;
|
|
||||||
pub use library::Library;
|
|
||||||
|
|
||||||
mod player_card;
|
|
||||||
pub use player_card::PlayerCard;
|
|
||||||
|
|
||||||
mod playing_field;
|
|
||||||
pub use playing_field::PlayingField;
|
|
||||||
|
|
||||||
mod home;
|
|
||||||
pub use home::Home;
|
|
||||||
|
|
||||||
mod board;
|
|
||||||
pub use board::Board;
|
|
||||||
|
|
||||||
#[cfg(feature = "screenplay")]
|
|
||||||
pub use playing_field::playing_field_view;
|
|
||||||
|
|
||||||
pub struct AppWindow {
|
|
||||||
pub window: adw::ApplicationWindow,
|
|
||||||
pub header: adw::HeaderBar,
|
|
||||||
pub content: adw::Bin,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AppWindow {
|
|
||||||
pub fn new(app: &adw::Application) -> Self {
|
|
||||||
let window = adw::ApplicationWindow::builder()
|
|
||||||
.application(app)
|
|
||||||
.width_request(800)
|
|
||||||
.height_request(500)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let stylesheet = String::from_utf8(
|
|
||||||
resources_lookup_data(
|
|
||||||
"/com/luminescent-dreams/kifu-gtk/style.css",
|
|
||||||
gio::ResourceLookupFlags::NONE,
|
|
||||||
)
|
|
||||||
.expect("stylesheet should just be available")
|
|
||||||
.to_vec(),
|
|
||||||
)
|
|
||||||
.expect("to parse stylesheet");
|
|
||||||
|
|
||||||
let provider = gtk::CssProvider::new();
|
|
||||||
provider.load_from_data(&stylesheet);
|
|
||||||
let context = window.style_context();
|
|
||||||
context.add_provider(&provider, STYLE_PROVIDER_PRIORITY_USER);
|
|
||||||
|
|
||||||
let header = adw::HeaderBar::builder()
|
|
||||||
.title_widget(>k::Label::new(Some("Kifu")))
|
|
||||||
.build();
|
|
||||||
|
|
||||||
let app_menu = gio::Menu::new();
|
|
||||||
let menu_item = gio::MenuItem::new(Some("Configuration"), Some("app.show-config"));
|
|
||||||
app_menu.append_item(&menu_item);
|
|
||||||
|
|
||||||
let hamburger = gtk::MenuButton::builder()
|
|
||||||
.icon_name("open-menu-symbolic")
|
|
||||||
.build();
|
|
||||||
hamburger.set_menu_model(Some(&app_menu));
|
|
||||||
|
|
||||||
header.pack_end(&hamburger);
|
|
||||||
|
|
||||||
let content = adw::Bin::builder().css_classes(vec!["content"]).build();
|
|
||||||
content.set_child(Some(
|
|
||||||
&adw::StatusPage::builder().title("Nothing here").build(),
|
|
||||||
));
|
|
||||||
|
|
||||||
let layout = gtk::Box::builder()
|
|
||||||
.orientation(gtk::Orientation::Vertical)
|
|
||||||
.build();
|
|
||||||
layout.append(&header);
|
|
||||||
layout.append(&content);
|
|
||||||
|
|
||||||
window.set_content(Some(&layout));
|
|
||||||
|
|
||||||
Self {
|
|
||||||
window,
|
|
||||||
header,
|
|
||||||
content,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_content(&self, content: &impl IsA<gtk::Widget>) {
|
|
||||||
self.content.set_child(Some(content));
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
use glib::Object;
|
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
|
||||||
use kifu_core::ui::PlayerCardElement;
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct PlayerCardPrivate {
|
|
||||||
player_name: gtk::Label,
|
|
||||||
clock: gtk::Label,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[glib::object_subclass]
|
|
||||||
impl ObjectSubclass for PlayerCardPrivate {
|
|
||||||
const NAME: &'static str = "PlayerCard";
|
|
||||||
type Type = PlayerCard;
|
|
||||||
type ParentType = gtk::Box;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObjectImpl for PlayerCardPrivate {}
|
|
||||||
impl WidgetImpl for PlayerCardPrivate {}
|
|
||||||
impl BoxImpl for PlayerCardPrivate {}
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct PlayerCard(ObjectSubclass<PlayerCardPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Orientable;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PlayerCard {
|
|
||||||
pub fn new(element: PlayerCardElement) -> PlayerCard {
|
|
||||||
let s: Self = Object::builder().build();
|
|
||||||
s.set_orientation(gtk::Orientation::Vertical);
|
|
||||||
s.imp()
|
|
||||||
.player_name
|
|
||||||
.set_text(&format!("{} ({})", element.name, element.rank));
|
|
||||||
s.imp().clock.set_text(&element.clock);
|
|
||||||
|
|
||||||
s.append(&s.imp().player_name);
|
|
||||||
s.append(&s.imp().clock);
|
|
||||||
s
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,38 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
|
||||||
|
|
||||||
This file is part of Kifu.
|
|
||||||
|
|
||||||
Kifu 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.
|
|
||||||
|
|
||||||
Kifu 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 Kifu. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::LocalObserver;
|
|
||||||
use kifu_core::{Core, CoreNotification};
|
|
||||||
|
|
||||||
pub struct GameReviewViewModel {
|
|
||||||
core: Core,
|
|
||||||
notification_observer: LocalObserver<CoreNotification>,
|
|
||||||
widget: gtk::Box,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GameReviewViewModel {
|
|
||||||
fn new(core: Core) -> Self {
|
|
||||||
let notification_observer = LocalObserver::new(&core, |msg| {
|
|
||||||
println!("GameReviewViewModel called with message: {:?}", msg)
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
core,
|
|
||||||
notification_observer,
|
|
||||||
widget: gtk::Box::new(gtk::Orientation::Horizontal, 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
|
||||||
|
|
||||||
This file is part of Kifu.
|
|
||||||
|
|
||||||
Kifu 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.
|
|
||||||
|
|
||||||
Kifu 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 Kifu. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use async_std::{channel::Receiver, task::yield_now};
|
|
||||||
use kifu_core::{Color, Core, CoreNotification, Goban, Observable, Player};
|
|
||||||
use std::{cell::RefCell, rc::Rc, time::Duration};
|
|
||||||
|
|
||||||
use crate::LocalObserver;
|
|
||||||
|
|
||||||
pub struct GameState {
|
|
||||||
goban: Goban,
|
|
||||||
white_clock: Duration,
|
|
||||||
black_clock: Duration,
|
|
||||||
white_score: f32,
|
|
||||||
black_score: f32,
|
|
||||||
current: Color,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GameViewModelPrivate {
|
|
||||||
white: Player, /* Maybe this should be PlayerState, instead, combining the player info, current clock, and current captures. */
|
|
||||||
black: Player,
|
|
||||||
|
|
||||||
state: GameState,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The Game View Model manages the current state of the game. It shows the two player cards, the board, the current capture count, the current player, and it maintains the UI for the clock (bearing in mind that the real clock is managed in the core). This view model should only be created once the details of the game, whether a game in progress or a new game (this view model won't know the difference) is known.
|
|
||||||
pub struct GameViewModel {
|
|
||||||
core: Core,
|
|
||||||
notification_observer: LocalObserver<CoreNotification>,
|
|
||||||
widget: gtk::Box,
|
|
||||||
data: Rc<RefCell<GameViewModelPrivate>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GameViewModelPrivate {
|
|
||||||
fn handle(&mut self, _message: CoreNotification) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl GameViewModel {
|
|
||||||
pub fn new(white: Player, black: Player, game: GameState, core: Core) -> Self {
|
|
||||||
let data = Rc::new(RefCell::new(GameViewModelPrivate {
|
|
||||||
white,
|
|
||||||
black,
|
|
||||||
state: game,
|
|
||||||
}));
|
|
||||||
|
|
||||||
let notification_observer = LocalObserver::new(&core, |msg| {
|
|
||||||
println!("GameViewModelHandler called with message: {:?}", msg)
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
core,
|
|
||||||
notification_observer,
|
|
||||||
widget: gtk::Box::new(gtk::Orientation::Horizontal, 0),
|
|
||||||
data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,54 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
|
||||||
|
|
||||||
This file is part of Kifu.
|
|
||||||
|
|
||||||
Kifu 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.
|
|
||||||
|
|
||||||
Kifu 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 Kifu. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::LocalObserver;
|
|
||||||
use kifu_core::{Core, CoreNotification};
|
|
||||||
|
|
||||||
/// Home controls the view that the user sees when starting the application if there are no games in progress. It provides a window into the database, showing a list of recently recorded games. It also provides the UI for starting a new game. This will render an empty database view if the user hasn't configured a database yet.
|
|
||||||
pub struct HomeViewModel {
|
|
||||||
core: Core,
|
|
||||||
notification_observer: LocalObserver<CoreNotification>,
|
|
||||||
widget: gtk::Box,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HomeViewModel {
|
|
||||||
fn new(core: Core) -> Self {
|
|
||||||
let notification_observer = LocalObserver::new(&core, |msg| {
|
|
||||||
println!("DatabaseViewModelHandler called with message: {:?}", msg)
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
core,
|
|
||||||
notification_observer,
|
|
||||||
widget: gtk::Box::new(gtk::Orientation::Horizontal, 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new game with the given parameters.
|
|
||||||
fn new_game(&self) {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Select a game from the database to show in detail. This will require a transition away from this view model into a different one.
|
|
||||||
fn select_game(&self) {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Delete a game from the database.
|
|
||||||
fn delete_game(&self) {
|
|
||||||
unimplemented!()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
|
||||||
|
|
||||||
This file is part of Kifu.
|
|
||||||
|
|
||||||
Kifu 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.
|
|
||||||
|
|
||||||
Kifu 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 Kifu. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/*
|
|
||||||
Every view model requires a reference to the app so that it can call functions on the core, and a notification receiver so that it can receive messages from the core.
|
|
||||||
|
|
||||||
The view model is primary over the view. It will construct the view, it can make major changes to the view or even swap for another related view. It must listen for all messages from the core, discarding those that aren't relevant to it. It will also convert requests from sync to async.
|
|
||||||
*/
|
|
||||||
|
|
||||||
mod game_view_model;
|
|
||||||
pub use game_view_model::GameViewModel;
|
|
||||||
|
|
||||||
mod game_review_view_model;
|
|
||||||
pub use game_review_view_model::GameReviewViewModel;
|
|
||||||
|
|
||||||
mod home_view_model;
|
|
||||||
pub use home_view_model::HomeViewModel;
|
|
||||||
|
|
||||||
mod settings_view_model;
|
|
||||||
pub use settings_view_model::SettingsViewModel;
|
|
|
@ -1,38 +0,0 @@
|
||||||
/*
|
|
||||||
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
|
||||||
|
|
||||||
This file is part of Kifu.
|
|
||||||
|
|
||||||
Kifu 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.
|
|
||||||
|
|
||||||
Kifu 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 Kifu. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use crate::LocalObserver;
|
|
||||||
use kifu_core::{Core, CoreNotification};
|
|
||||||
|
|
||||||
pub struct SettingsViewModel {
|
|
||||||
core: Core,
|
|
||||||
notification_observer: LocalObserver<CoreNotification>,
|
|
||||||
widget: gtk::Box,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SettingsViewModel {
|
|
||||||
fn new(core: Core) -> Self {
|
|
||||||
let notification_observer = LocalObserver::new(&core, |msg| {
|
|
||||||
println!("SettingsViewModel called with message: {:?}", msg)
|
|
||||||
});
|
|
||||||
|
|
||||||
Self {
|
|
||||||
core,
|
|
||||||
notification_observer,
|
|
||||||
widget: gtk::Box::new(gtk::Orientation::Horizontal, 0),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
[package]
|
[package]
|
||||||
name = "kifu-core"
|
name = "otg-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
|
@ -14,7 +14,9 @@ sgf = { path = "../../sgf" }
|
||||||
grid = { version = "0.9" }
|
grid = { version = "0.9" }
|
||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
serde = { version = "1", features = [ "derive" ] }
|
serde = { version = "1", features = [ "derive" ] }
|
||||||
|
nary_tree = { version = "0.4" }
|
||||||
thiserror = { version = "1" }
|
thiserror = { version = "1" }
|
||||||
|
uuid = { version = "0.8", features = ["v4", "serde"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
cool_asserts = { version = "2" }
|
cool_asserts = { version = "2" }
|
|
@ -1,21 +1,37 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
database::Database,
|
database::Database,
|
||||||
types::{AppState, Config, ConfigOption, DatabasePath, GameState, Player, Rank},
|
library, settings,
|
||||||
ui::{configuration, home, playing_field, ConfigurationView, HomeView, PlayingFieldView},
|
types::{Config, LibraryPath},
|
||||||
};
|
};
|
||||||
use async_std::channel::{Receiver, Sender};
|
use async_std::channel::{Receiver, Sender};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::sync::{Arc, RwLock, RwLockReadGuard};
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, RwLock},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub trait Observable<T> {
|
pub trait Observable<T> {
|
||||||
fn subscribe(&self) -> Receiver<T>;
|
fn subscribe(&self) -> Receiver<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum CoreRequest {
|
pub enum CoreRequest {
|
||||||
|
Library(library::LibraryRequest),
|
||||||
|
Settings(settings::SettingsRequest),
|
||||||
|
/*
|
||||||
ChangeSetting(ChangeSettingRequest),
|
ChangeSetting(ChangeSettingRequest),
|
||||||
CreateGame(CreateGameRequest),
|
CreateGame(CreateGameRequest),
|
||||||
Home,
|
Home,
|
||||||
|
@ -23,8 +39,10 @@ pub enum CoreRequest {
|
||||||
PlayingField,
|
PlayingField,
|
||||||
PlayStone(PlayStoneRequest),
|
PlayStone(PlayStoneRequest),
|
||||||
StartGame,
|
StartGame,
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum ChangeSettingRequest {
|
pub enum ChangeSettingRequest {
|
||||||
LibraryPath(String),
|
LibraryPath(String),
|
||||||
|
@ -61,42 +79,104 @@ impl From<HotseatPlayerRequest> for Player {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum CoreResponse {
|
pub enum CoreResponse {
|
||||||
ConfigurationView(ConfigurationView),
|
Library(library::LibraryResponse),
|
||||||
HomeView(HomeView),
|
Settings(settings::SettingsResponse),
|
||||||
PlayingFieldView(PlayingFieldView),
|
|
||||||
UpdatedConfigurationView(ConfigurationView),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
impl From<library::LibraryResponse> for CoreResponse {
|
||||||
|
fn from(r: library::LibraryResponse) -> Self {
|
||||||
|
Self::Library(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<settings::SettingsResponse> for CoreResponse {
|
||||||
|
fn from(r: settings::SettingsResponse) -> Self {
|
||||||
|
Self::Settings(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
pub enum CoreNotification {
|
pub enum CoreNotification {
|
||||||
|
ConfigurationUpdated(Config),
|
||||||
BoardUpdated,
|
BoardUpdated,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Core {
|
pub struct Core {
|
||||||
// config: Arc<RwLock<Config>>,
|
config: Arc<RwLock<Config>>,
|
||||||
// state: Arc<RwLock<AppState>>,
|
// state: Arc<RwLock<AppState>>,
|
||||||
database: Arc<RwLock<Option<Database>>>,
|
library: Arc<RwLock<Option<Database>>>,
|
||||||
subscribers: Arc<RwLock<Vec<Sender<CoreNotification>>>>,
|
subscribers: Arc<RwLock<Vec<Sender<CoreNotification>>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Core {
|
impl Core {
|
||||||
pub fn new(_config: Config) -> Self {
|
pub fn new(config: Config) -> Self {
|
||||||
// let config = Config::from_path(config_path).expect("configuration to open");
|
let library = match config.get::<LibraryPath>() {
|
||||||
|
Some(ref path) if path.to_path_buf().exists() => {
|
||||||
// let state = Arc::new(RwLock::new(AppState::new(db_path)));
|
Some(Database::open_path(path.to_path_buf()).unwrap())
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
// config: Arc::new(RwLock::new(config)),
|
config: Arc::new(RwLock::new(config)),
|
||||||
// state,
|
// state,
|
||||||
database: Arc::new(RwLock::new(None)),
|
library: Arc::new(RwLock::new(library)),
|
||||||
subscribers: Arc::new(RwLock::new(vec![])),
|
subscribers: Arc::new(RwLock::new(vec![])),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_config(&self) -> Config {
|
||||||
|
self.config.read().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the configuration of the Core. This function will update any relevant core
|
||||||
|
/// functions, especially the contents of the library, and it will notify any subscribed objects
|
||||||
|
/// that the configuration has changed.
|
||||||
|
///
|
||||||
|
/// It will not handle persisting the new configuration, as the backing store for the
|
||||||
|
/// configuration is not a decision for the core library.
|
||||||
|
pub async fn set_config(&self, config: Config) {
|
||||||
|
*self.config.write().unwrap() = config.clone();
|
||||||
|
|
||||||
|
// let db = library::read_library(self.config.read().unwrap().get::<LibraryPath>()).await;
|
||||||
|
let library_path = self.config.read().unwrap().get::<LibraryPath>();
|
||||||
|
if let Some(ref path) = library_path {
|
||||||
|
self.load_library(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.notify(CoreNotification::ConfigurationUpdated(config.clone()))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_library(&self, path: &LibraryPath) {
|
||||||
|
let db = Database::open_path(path.to_path_buf()).unwrap();
|
||||||
|
*self.library.write().unwrap() = Some(db);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn library(&self) -> RwLockReadGuard<'_, Option<Database>> {
|
||||||
|
self.library.read().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
|
||||||
|
match request {
|
||||||
|
CoreRequest::Library(request) => library::handle(self, request).await.into(),
|
||||||
|
CoreRequest::Settings(request) => settings::handle(self, request).await.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn notify(&self, notification: CoreNotification) {
|
||||||
|
let subscribers = self.subscribers.read().unwrap().clone();
|
||||||
|
for subscriber in subscribers {
|
||||||
|
let subscriber = subscriber.clone();
|
||||||
|
let _ = subscriber.send(notification.clone()).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
|
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
|
||||||
match request {
|
match request {
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{io::Read, path::PathBuf};
|
use std::{io::Read, path::PathBuf};
|
||||||
|
|
||||||
use sgf::{parse_sgf, Game};
|
use sgf::{parse_sgf, GameRecord};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
@ -21,12 +21,12 @@ impl From<std::io::Error> for Error {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
games: Vec<Game>,
|
games: Vec<GameRecord>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub fn open_path(path: PathBuf) -> Result<Database, Error> {
|
pub fn open_path(path: PathBuf) -> Result<Database, Error> {
|
||||||
let mut games: Vec<Game> = Vec::new();
|
let mut games: Vec<GameRecord> = Vec::new();
|
||||||
|
|
||||||
let extension = PathBuf::from("sgf").into_os_string();
|
let extension = PathBuf::from("sgf").into_os_string();
|
||||||
|
|
||||||
|
@ -42,9 +42,9 @@ impl Database {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
match parse_sgf(&buffer) {
|
match parse_sgf(&buffer) {
|
||||||
Ok(sgfs) => {
|
Ok(sgfs) => {
|
||||||
for sgf in sgfs {
|
let mut sgfs =
|
||||||
games.push(sgf);
|
sgfs.into_iter().flatten().collect::<Vec<sgf::GameRecord>>();
|
||||||
}
|
games.append(&mut sgfs);
|
||||||
}
|
}
|
||||||
Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err),
|
Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err),
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ impl Database {
|
||||||
Ok(Database { games })
|
Ok(Database { games })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_games(&self) -> impl Iterator<Item = &Game> {
|
pub fn all_games(&self) -> impl Iterator<Item = &GameRecord> {
|
||||||
self.games.iter()
|
self.games.iter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -82,11 +82,11 @@ mod test {
|
||||||
Database::open_path(PathBuf::from("fixtures/five_games/")).expect("database to open");
|
Database::open_path(PathBuf::from("fixtures/five_games/")).expect("database to open");
|
||||||
assert_eq!(db.all_games().count(), 5);
|
assert_eq!(db.all_games().count(), 5);
|
||||||
|
|
||||||
assert_matches!(db.all_games().find(|g| g.info.black_player == Some("Steve".to_owned())),
|
assert_matches!(db.all_games().find(|g| g.black_player.name == Some("Steve".to_owned())),
|
||||||
Some(game) => {
|
Some(game) => {
|
||||||
assert_eq!(game.info.black_player, Some("Steve".to_owned()));
|
assert_eq!(game.black_player.name, Some("Steve".to_owned()));
|
||||||
assert_eq!(game.info.white_player, Some("Savanni".to_owned()));
|
assert_eq!(game.white_player.name, Some("Savanni".to_owned()));
|
||||||
assert_eq!(game.info.date, vec![Date::Date(chrono::NaiveDate::from_ymd_opt(2023, 4, 19).unwrap())]);
|
assert_eq!(game.dates, vec![Date::Date(chrono::NaiveDate::from_ymd_opt(2023, 4, 19).unwrap())]);
|
||||||
// assert_eq!(game.info.komi, Some(6.5));
|
// assert_eq!(game.info.komi, Some(6.5));
|
||||||
}
|
}
|
||||||
);
|
);
|
|
@ -1,9 +1,35 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TBQH, I don't recall what state this object is in, but I do know that I might have some troubles
|
||||||
|
// integrating it with a game record. Some of the time here is going to be me reading (and
|
||||||
|
// documenting) my code from almost a year ago.
|
||||||
|
//
|
||||||
use crate::{BoardError, Color, Size};
|
use crate::{BoardError, Color, Size};
|
||||||
|
use sgf::{GameNode, MoveNode};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct Goban {
|
pub struct Goban {
|
||||||
|
/// The size of the board. Usually this is symetrical, but I have actually played a 5x25 game.
|
||||||
|
/// These are fun for novelty, but don't lend much to understanding the game.
|
||||||
pub size: Size,
|
pub size: Size,
|
||||||
|
|
||||||
|
/// I found that it was easiest to track groups of stones than to track individual stones on the
|
||||||
|
/// board. So, I just keep track of all of the groups.
|
||||||
pub groups: Vec<Group>,
|
pub groups: Vec<Group>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,10 +88,18 @@ impl Goban {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate a board state from an iterator of coordinates and the color of any stone present on
|
||||||
|
/// the board. As we walk through the iterator, we play each stone as though it were being
|
||||||
|
/// played in a game.
|
||||||
|
///
|
||||||
|
/// This would not work at all if we wanted to set up an impossible board state, given that
|
||||||
|
/// groups of stones get automatically removed once surrounded.
|
||||||
pub fn from_coordinates(
|
pub fn from_coordinates(
|
||||||
mut coordinates: impl Iterator<Item = (Coordinate, Color)>,
|
coordinates: impl IntoIterator<Item = (Coordinate, Color)>,
|
||||||
) -> Result<Self, BoardError> {
|
) -> Result<Self, BoardError> {
|
||||||
coordinates.try_fold(Self::new(), |board, (coordinate, color)| {
|
coordinates
|
||||||
|
.into_iter()
|
||||||
|
.try_fold(Self::new(), |board, (coordinate, color)| {
|
||||||
board.place_stone(coordinate, color)
|
board.place_stone(coordinate, color)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -78,37 +112,77 @@ pub struct Coordinate {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Goban {
|
impl Goban {
|
||||||
|
/// place_stone is the most fundamental function of this object. This is as though a player put
|
||||||
|
/// a stone on the board and evaluated the consequences.
|
||||||
|
///
|
||||||
|
/// This function does not enforce turn order.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use otg_core::{Color, Size, Coordinate, Goban};
|
||||||
|
/// use cool_asserts::assert_matches;
|
||||||
|
///
|
||||||
|
/// let goban = Goban::new();
|
||||||
|
/// assert_eq!(goban.size, Size{ width: 19, height: 19 });
|
||||||
|
/// let move_result = goban.place_stone(Coordinate{ column: 4, row: 4 }, Color::Black);
|
||||||
|
/// assert_matches!(move_result, Goban);
|
||||||
|
/// ```
|
||||||
pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result<Self, BoardError> {
|
pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result<Self, BoardError> {
|
||||||
|
// Bail out immediately if there is already a stone at this location.
|
||||||
if self.stone(&coordinate).is_some() {
|
if self.stone(&coordinate).is_some() {
|
||||||
return Err(BoardError::InvalidPosition);
|
return Err(BoardError::InvalidPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find all friendly groups adjacent to this stone. First, calculate the adjacent
|
||||||
|
// coordinates. Then see if there is any group which contains that coordinate. If not, this
|
||||||
|
// stone forms a new group of its own.
|
||||||
|
//
|
||||||
|
// A little subtle here is that this stone will be added to *every* adjoining friendly
|
||||||
|
// group. This normally means only that a group gets bigger, but it could also cause two
|
||||||
|
// groups to share a stone, which means they're now a single group.
|
||||||
let mut friendly_group = self
|
let mut friendly_group = self
|
||||||
.adjacencies(&coordinate)
|
.adjacencies(&coordinate)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|c| self.stone(c) == Some(color))
|
.filter(|c| self.stone(c) == Some(color))
|
||||||
.filter_map(|c| self.group(&c).map(|g| g.coordinates.clone()))
|
.filter_map(|c| self.group(&c).map(|g| g.coordinates.clone()))
|
||||||
|
// In fact, this last step actually connects the coordinates of those friendly groups
|
||||||
|
// into a single large group.
|
||||||
.fold(HashSet::new(), |acc, set| {
|
.fold(HashSet::new(), |acc, set| {
|
||||||
acc.union(&set).cloned().collect()
|
acc.union(&set).cloned().collect()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// This is a little misnamed. This is a HashSet, not a full Group.
|
||||||
friendly_group.insert(coordinate);
|
friendly_group.insert(coordinate);
|
||||||
|
|
||||||
|
// Remove all groups which contain the stones overlapping with this friendly group.
|
||||||
self.groups
|
self.groups
|
||||||
.retain(|g| g.coordinates.is_disjoint(&friendly_group));
|
.retain(|g| g.coordinates.is_disjoint(&friendly_group));
|
||||||
|
|
||||||
|
// Generate a new friendly group given the coordinates.
|
||||||
let friendly_group = Group {
|
let friendly_group = Group {
|
||||||
color,
|
color,
|
||||||
coordinates: friendly_group,
|
coordinates: friendly_group,
|
||||||
};
|
};
|
||||||
|
// Now add the group back to the board.
|
||||||
self.groups.push(friendly_group.clone());
|
self.groups.push(friendly_group.clone());
|
||||||
|
|
||||||
|
// Now, find all groups adjacent to this one. Those are the only groups that this move is
|
||||||
|
// going to impact. Calculate their liberties.
|
||||||
let adjacent_groups = self.adjacent_groups(&friendly_group);
|
let adjacent_groups = self.adjacent_groups(&friendly_group);
|
||||||
for group in adjacent_groups {
|
for group in adjacent_groups {
|
||||||
|
// Any group that has been reduced to 0 liberties should now be removed from the board.
|
||||||
|
//
|
||||||
|
// TODO: capture rules: we're not counting captured stones yet. Okay with some scoring
|
||||||
|
// methods, but not all.
|
||||||
if self.liberties(&group) == 0 {
|
if self.liberties(&group) == 0 {
|
||||||
self.remove_group(&group);
|
self.remove_group(&group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now, recalculate the liberties of this friendly group. If this group has been reduced to
|
||||||
|
// zero liberties, after all captures have been accounted for, the move is an illegal
|
||||||
|
// self-capture. Drop all of the work we've done and return an error.
|
||||||
if self.liberties(&friendly_group) == 0 {
|
if self.liberties(&friendly_group) == 0 {
|
||||||
return Err(BoardError::SelfCapture);
|
return Err(BoardError::SelfCapture);
|
||||||
}
|
}
|
||||||
|
@ -116,6 +190,53 @@ impl Goban {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply a list of moves to the board and return the final board. The moves will be played as
|
||||||
|
/// though they are live moves played normally, but this function is for generating a board
|
||||||
|
/// state from a game record. All of the moves will be played in the order given. This does not
|
||||||
|
/// allow for the branching which is natural in a game review.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use otg_core::{Color, Size, Coordinate, Goban};
|
||||||
|
/// use cool_asserts::assert_matches;
|
||||||
|
/// use sgf::{GameNode, MoveNode, Move};
|
||||||
|
///
|
||||||
|
/// let goban = Goban::new();
|
||||||
|
/// let moves = vec![
|
||||||
|
/// GameNode::MoveNode(MoveNode::new(sgf::Color::Black, Move::Move("dd".to_owned()))),
|
||||||
|
/// GameNode::MoveNode(MoveNode::new(sgf::Color::White, Move::Move("pp".to_owned()))),
|
||||||
|
/// GameNode::MoveNode(MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()))),
|
||||||
|
/// ];
|
||||||
|
/// let moves_: Vec<&GameNode> = moves.iter().collect();
|
||||||
|
/// let goban = goban.apply_moves(moves_).expect("the test to have valid moves");
|
||||||
|
///
|
||||||
|
/// 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: 15, column: 3 }), Some(Color::Black));
|
||||||
|
/// ```
|
||||||
|
pub fn apply_moves<'a>(
|
||||||
|
self,
|
||||||
|
moves: impl IntoIterator<Item = &'a GameNode>,
|
||||||
|
) -> Result<Goban, BoardError> {
|
||||||
|
let mut s = self;
|
||||||
|
for m in moves.into_iter() {
|
||||||
|
match m {
|
||||||
|
GameNode::MoveNode(node) => s = s.apply_move_node(node)?,
|
||||||
|
GameNode::SetupNode(_n) => unimplemented!("setup nodes aren't processed yet"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Ok(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_move_node(self, m: &MoveNode) -> Result<Goban, BoardError> {
|
||||||
|
if let Some((row, column)) = m.mv.coordinate() {
|
||||||
|
self.place_stone(Coordinate { row, column }, Color::from(&m.color))
|
||||||
|
} else {
|
||||||
|
Ok(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn stone(&self, coordinate: &Coordinate) -> Option<Color> {
|
pub fn stone(&self, coordinate: &Coordinate) -> Option<Color> {
|
||||||
self.groups
|
self.groups
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -123,17 +244,17 @@ impl Goban {
|
||||||
.map(|g| g.color)
|
.map(|g| g.color)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
|
fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
|
||||||
self.groups
|
self.groups
|
||||||
.iter()
|
.iter()
|
||||||
.find(|g| g.coordinates.contains(coordinate))
|
.find(|g| g.coordinates.contains(coordinate))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_group(&mut self, group: &Group) {
|
fn remove_group(&mut self, group: &Group) {
|
||||||
self.groups.retain(|g| g != group);
|
self.groups.retain(|g| g != group);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
|
fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
|
||||||
let adjacent_spaces = self.group_halo(group).into_iter();
|
let adjacent_spaces = self.group_halo(group).into_iter();
|
||||||
let mut grps: Vec<Group> = Vec::new();
|
let mut grps: Vec<Group> = Vec::new();
|
||||||
|
|
||||||
|
@ -153,7 +274,7 @@ impl Goban {
|
||||||
grps
|
grps
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
|
fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
|
||||||
group
|
group
|
||||||
.coordinates
|
.coordinates
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -161,14 +282,14 @@ impl Goban {
|
||||||
.collect::<HashSet<Coordinate>>()
|
.collect::<HashSet<Coordinate>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn liberties(&self, group: &Group) -> usize {
|
fn liberties(&self, group: &Group) -> usize {
|
||||||
self.group_halo(group)
|
self.group_halo(group)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|c| self.stone(c).is_none())
|
.filter(|c| self.stone(c).is_none())
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
|
fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
|
||||||
let mut v = Vec::new();
|
let mut v = Vec::new();
|
||||||
if coordinate.column > 0 {
|
if coordinate.column > 0 {
|
||||||
v.push(Coordinate {
|
v.push(Coordinate {
|
||||||
|
@ -193,7 +314,7 @@ impl Goban {
|
||||||
v.into_iter().filter(|c| self.within_board(c)).collect()
|
v.into_iter().filter(|c| self.within_board(c)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn within_board(&self, coordinate: &Coordinate) -> bool {
|
fn within_board(&self, coordinate: &Coordinate) -> bool {
|
||||||
coordinate.column < self.size.width && coordinate.row < self.size.height
|
coordinate.column < self.size.width && coordinate.row < self.size.height
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -490,7 +611,6 @@ mod test {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
println!("{}", board);
|
|
||||||
for (board, coordinate, group, liberties) in test_cases {
|
for (board, coordinate, group, liberties) in test_cases {
|
||||||
assert_eq!(board.group(&coordinate), group.as_ref());
|
assert_eq!(board.group(&coordinate), group.as_ref());
|
||||||
assert_eq!(
|
assert_eq!(
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
extern crate config_derive;
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
pub use api::{Core, CoreNotification, CoreRequest, CoreResponse, Observable};
|
||||||
|
|
||||||
|
mod goban;
|
||||||
|
pub use goban::*;
|
||||||
|
|
||||||
|
mod database;
|
||||||
|
|
||||||
|
pub mod library;
|
||||||
|
|
||||||
|
pub mod settings;
|
||||||
|
|
||||||
|
mod types;
|
||||||
|
pub use types::{
|
||||||
|
BoardError, Color, Config, ConfigOption, DepthTree, LibraryPath, Player, Rank, Size,
|
||||||
|
};
|
||||||
|
|
||||||
|
mod view_models;
|
||||||
|
pub use view_models::GameReviewViewModel;
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::Core;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sgf::GameRecord;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum LibraryRequest {
|
||||||
|
ListGames,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum LibraryResponse {
|
||||||
|
Games(Vec<GameRecord>),
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_list_games(model: &Core) -> LibraryResponse {
|
||||||
|
let library = model.library();
|
||||||
|
match *library {
|
||||||
|
Some(ref library) => {
|
||||||
|
let info = library.all_games().cloned().collect::<Vec<GameRecord>>();
|
||||||
|
LibraryResponse::Games(info)
|
||||||
|
}
|
||||||
|
None => LibraryResponse::Games(vec![]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn handle(model: &Core, request: LibraryRequest) -> LibraryResponse {
|
||||||
|
match request {
|
||||||
|
LibraryRequest::ListGames => handle_list_games(model).await,
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
use crate::{Core, Config};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum SettingsRequest {
|
||||||
|
Get,
|
||||||
|
Set(Config),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SettingsResponse(pub Config);
|
||||||
|
|
||||||
|
pub async fn handle(model: &Core, request: SettingsRequest) -> SettingsResponse {
|
||||||
|
match request {
|
||||||
|
SettingsRequest::Get => SettingsResponse(model.get_config()),
|
||||||
|
SettingsRequest::Set(config) => {
|
||||||
|
model.set_config(config).await;
|
||||||
|
SettingsResponse(model.get_config())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,758 @@
|
||||||
|
use crate::goban::{Coordinate, Goban};
|
||||||
|
use config::define_config;
|
||||||
|
use config_derive::ConfigOption;
|
||||||
|
use nary_tree::NodeRef;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sgf::GameTree;
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, VecDeque}, fmt, ops::Deref, path::PathBuf, time::Duration
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
define_config! {
|
||||||
|
LibraryPath(LibraryPath),
|
||||||
|
Me(Me),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
|
||||||
|
pub struct LibraryPath(pub PathBuf);
|
||||||
|
|
||||||
|
impl std::ops::Deref for LibraryPath {
|
||||||
|
type Target = PathBuf;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<String> for LibraryPath {
|
||||||
|
fn from(s: String) -> Self {
|
||||||
|
Self(PathBuf::from(s))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, ConfigOption)]
|
||||||
|
pub struct Me(Player);
|
||||||
|
|
||||||
|
impl std::ops::Deref for Me {
|
||||||
|
type Target = Player;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Error)]
|
||||||
|
pub enum BoardError {
|
||||||
|
#[error("Position is invalid")]
|
||||||
|
InvalidPosition,
|
||||||
|
#[error("Self-capture is forbidden")]
|
||||||
|
SelfCapture,
|
||||||
|
#[error("Ko")]
|
||||||
|
Ko,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Color {
|
||||||
|
Black,
|
||||||
|
White,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&sgf::Color> for Color {
|
||||||
|
fn from(c: &sgf::Color) -> Self {
|
||||||
|
match c {
|
||||||
|
sgf::Color::Black => Self::Black,
|
||||||
|
sgf::Color::White => Self::White,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Color {
|
||||||
|
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
|
||||||
|
match self {
|
||||||
|
Color::Black => write!(formatter, "Black"),
|
||||||
|
Color::White => write!(formatter, "White"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct Size {
|
||||||
|
pub width: u8,
|
||||||
|
pub height: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Size {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
width: 19,
|
||||||
|
height: 19,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AppState stores all of the important state to a full running version of the application.
|
||||||
|
/// However, this version of AppState is in pretty sorry shape.
|
||||||
|
///
|
||||||
|
/// What are the states of the app?
|
||||||
|
///
|
||||||
|
/// - in review
|
||||||
|
/// - in a game
|
||||||
|
/// - connections to the internet
|
||||||
|
/// - connections to local applications, such as Leela Zero
|
||||||
|
/// - the current configuration
|
||||||
|
/// - the games database
|
||||||
|
/// - If in a game, the current state of the game. Delegated to GameState.
|
||||||
|
/// - If in review, the current state of the review.
|
||||||
|
///
|
||||||
|
/// Some of these states are concurrent. It's quite possible to have online connections running and
|
||||||
|
/// to be reviewing a game while, for instance, waiting for an opponent on OGS to make a move.
|
||||||
|
///
|
||||||
|
/// I get to ignore a lot of these things for now. Not playing online. Not playing at all,
|
||||||
|
/// actually. We'll come back to that.
|
||||||
|
///
|
||||||
|
/// Plus, it gets more fuzzy, because some of the application state is really UI state. For
|
||||||
|
/// instance, the state of a game review is purely UI.
|
||||||
|
///
|
||||||
|
/// So... AppState probably isn't great for now, but maybe it will become so later. I think I'm
|
||||||
|
/// going to ignore it until I need it.
|
||||||
|
/*
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AppState {
|
||||||
|
pub game: Option<GameState>,
|
||||||
|
pub database: Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppState {
|
||||||
|
pub fn new(database_path: LibraryPath) -> Self {
|
||||||
|
Self {
|
||||||
|
game: Some(GameState::default()),
|
||||||
|
database: Database::open_path(database_path.to_path_buf()).unwrap(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
pub fn place_stone(&mut self, req: PlayStoneRequest) {
|
||||||
|
if let Some(ref mut game) = self.game {
|
||||||
|
let _ = game.place_stone(Coordinate {
|
||||||
|
column: req.column,
|
||||||
|
row: req.row,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum Rank {
|
||||||
|
Kyu(u8),
|
||||||
|
Dan(u8),
|
||||||
|
Pro(u8),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for Rank {
|
||||||
|
type Error = String;
|
||||||
|
|
||||||
|
fn try_from(_: &str) -> Result<Rank, Self::Error> {
|
||||||
|
Ok(Rank::Kyu(15))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Rank> for String {
|
||||||
|
fn from(r: Rank) -> String {
|
||||||
|
match r {
|
||||||
|
Rank::Kyu(v) => format!("{} kyu", v),
|
||||||
|
Rank::Dan(v) => format!("{} dan", v),
|
||||||
|
Rank::Pro(v) => format!("{} pro", v),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Player {
|
||||||
|
pub name: String,
|
||||||
|
pub rank: Option<Rank>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct GameState {
|
||||||
|
pub board: Goban,
|
||||||
|
pub past_positions: Vec<Goban>,
|
||||||
|
|
||||||
|
pub conversation: Vec<String>,
|
||||||
|
pub current_player: Color,
|
||||||
|
|
||||||
|
pub white_player: Player,
|
||||||
|
pub black_player: Player,
|
||||||
|
|
||||||
|
pub white_clock: Duration,
|
||||||
|
pub black_clock: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GameState {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
board: Goban::new(),
|
||||||
|
past_positions: vec![],
|
||||||
|
conversation: vec![],
|
||||||
|
current_player: Color::Black,
|
||||||
|
white_player: Player {
|
||||||
|
name: "".to_owned(),
|
||||||
|
rank: None,
|
||||||
|
},
|
||||||
|
black_player: Player {
|
||||||
|
name: "".to_owned(),
|
||||||
|
rank: None,
|
||||||
|
},
|
||||||
|
white_clock: Duration::from_secs(600),
|
||||||
|
black_clock: Duration::from_secs(600),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameState {
|
||||||
|
// Legacy code. I recall that this is no longer used (but will be used again) because I
|
||||||
|
// commented out so much code when I was overhauling the architecture of this app.
|
||||||
|
#[allow(dead_code)]
|
||||||
|
fn place_stone(&mut self, coordinate: Coordinate) -> Result<(), BoardError> {
|
||||||
|
let board = self.board.clone();
|
||||||
|
let new_board = board.place_stone(coordinate, self.current_player)?;
|
||||||
|
|
||||||
|
if self.past_positions.contains(&new_board) {
|
||||||
|
return Err(BoardError::Ko);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.past_positions.push(self.board.clone());
|
||||||
|
self.board = new_board;
|
||||||
|
match self.current_player {
|
||||||
|
Color::White => self.current_player = Color::Black,
|
||||||
|
Color::Black => self.current_player = Color::White,
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
// 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.
|
||||||
|
//
|
||||||
|
// So, what is the maximum depth of the tree? Follow all paths and see how far I get in every case.
|
||||||
|
// I could do this by just generating an intermediate tree and numbering each level.
|
||||||
|
pub struct Tree<T> {
|
||||||
|
nodes: Vec<Node<T>>,
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 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 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 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
|
||||||
|
// to add the root node, but the constructor should handle that, anyway.
|
||||||
|
fn add_node(&mut self, parent_idx: usize, node: T) -> usize {
|
||||||
|
let next_idx = self.nodes.len();
|
||||||
|
let parent = &self.nodes[parent_idx];
|
||||||
|
|
||||||
|
self.nodes.push(Node {
|
||||||
|
id: next_idx,
|
||||||
|
content: node,
|
||||||
|
parent: Some(parent_idx),
|
||||||
|
depth: parent.depth + 1,
|
||||||
|
width: RefCell::new(None),
|
||||||
|
children: vec![],
|
||||||
|
});
|
||||||
|
|
||||||
|
let parent = &mut self.nodes[parent_idx];
|
||||||
|
parent.children.push(next_idx);
|
||||||
|
next_idx
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub fn max_depth(&self) -> usize {
|
||||||
|
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
|
||||||
|
// scheme of things.
|
||||||
|
//
|
||||||
|
// One thought I have is that I could just develop a grid virtually and start placing nodes.
|
||||||
|
// Whenever I notice a collision, I can just move the node over. But I'd like to see if I can
|
||||||
|
// be a bit smarter than doing it as just a vec into which I place things, as though it's a
|
||||||
|
// game board. So, given a game node, I want to figure out it's position along the X axis.
|
||||||
|
//
|
||||||
|
// Just having the node is greatly insufficient. I can get better results if I'm calculating
|
||||||
|
// the position of its children.
|
||||||
|
//
|
||||||
|
// indent represents the indentation that should be applied to all children in this tree. It
|
||||||
|
// amounts to the position of the parent node.
|
||||||
|
//
|
||||||
|
// When drawing nodes, I don't know how to persist the level of indent.
|
||||||
|
|
||||||
|
// unimplemented!()
|
||||||
|
/*
|
||||||
|
let node = &self.nodes[idx];
|
||||||
|
match node.parent {
|
||||||
|
Some(parent_idx) => {
|
||||||
|
let (_parent_row, parent_column) = self.position(parent_idx);
|
||||||
|
let parent = &self.nodes[parent_idx];
|
||||||
|
let sibling_width = parent
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.take_while(|n| **n != node.id)
|
||||||
|
.fold(0, |acc, n| acc + self.width(*n));
|
||||||
|
(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.
|
||||||
|
//
|
||||||
|
// There are some collapse rules that I could take into account here, but that I haven't
|
||||||
|
// figured out yet. If two nodes are side by side, and one of them has some wide children but
|
||||||
|
// the other has no children, then they are effectively the same width. The second node only
|
||||||
|
// needs to be moved out if it has children that would overlap the children of the first node.
|
||||||
|
//
|
||||||
|
// My algorithm right now is likely to generate unnecessarily wide trees in a complex game
|
||||||
|
// review.
|
||||||
|
fn width(&self, id: usize) -> usize {
|
||||||
|
let node = &self.nodes[id];
|
||||||
|
if let Some(width) = *node.width.borrow() {
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = node
|
||||||
|
.children
|
||||||
|
.iter()
|
||||||
|
.fold(0, |acc, child| acc + self.width(*child));
|
||||||
|
let width = if width == 0 { 1 } else { width };
|
||||||
|
*node.width.borrow_mut() = Some(width);
|
||||||
|
|
||||||
|
width
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub fn bfs_iter(&self) -> BFSIter<'_, SizeNode> {
|
||||||
|
let mut queue = VecDeque::new();
|
||||||
|
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(tree: &mut Tree<Uuid>, parent_idx: usize, node: &GameNode) {
|
||||||
|
let idx = tree.add_node(parent_idx, node.id());
|
||||||
|
|
||||||
|
let children = match node {
|
||||||
|
GameNode::MoveNode(node) => &node.children,
|
||||||
|
GameNode::SetupNode(node) => &node.children,
|
||||||
|
};
|
||||||
|
|
||||||
|
for child in children {
|
||||||
|
add_subtree(tree, idx, child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut tree = Tree::new(root.id());
|
||||||
|
|
||||||
|
let children = match root {
|
||||||
|
GameNode::MoveNode(node) => &node.children,
|
||||||
|
GameNode::SetupNode(node) => &node.children,
|
||||||
|
};
|
||||||
|
|
||||||
|
for node in children {
|
||||||
|
add_subtree(&mut tree, 0, node);
|
||||||
|
}
|
||||||
|
|
||||||
|
tree
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
pub struct BFSIter<'a, T> {
|
||||||
|
queue: VecDeque<nary_tree::NodeRef<'a, T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Iterator for BFSIter<'a, 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()
|
||||||
|
.for_each(|noderef| self.queue.push_back(noderef));
|
||||||
|
}
|
||||||
|
retval
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use super::*;
|
||||||
|
// use sgf::{GameRecord, GameTree, GameType, Move, MoveNode};
|
||||||
|
use sgf::{GameNode, GameTree, Move, MoveNode};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn current_player_changes_after_move() {
|
||||||
|
let mut state = GameState::default();
|
||||||
|
assert_eq!(state.current_player, Color::Black);
|
||||||
|
state.place_stone(Coordinate { column: 9, row: 9 }).unwrap();
|
||||||
|
assert_eq!(state.current_player, Color::White);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn current_player_remains_the_same_after_self_capture() {
|
||||||
|
let mut state = GameState::default();
|
||||||
|
state.board = Goban::from_coordinates(
|
||||||
|
vec![
|
||||||
|
(Coordinate { column: 17, row: 0 }, Color::White),
|
||||||
|
(Coordinate { column: 17, row: 1 }, Color::White),
|
||||||
|
(Coordinate { column: 18, row: 1 }, Color::White),
|
||||||
|
]
|
||||||
|
.into_iter(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
state.current_player = Color::Black;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
state.place_stone(Coordinate { column: 18, row: 0 }),
|
||||||
|
Err(BoardError::SelfCapture)
|
||||||
|
);
|
||||||
|
assert_eq!(state.current_player, Color::Black);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ko_rules_are_enforced() {
|
||||||
|
let mut state = GameState::default();
|
||||||
|
state.board = Goban::from_coordinates(
|
||||||
|
vec![
|
||||||
|
(Coordinate { column: 7, row: 9 }, Color::White),
|
||||||
|
(Coordinate { column: 8, row: 8 }, Color::White),
|
||||||
|
(Coordinate { column: 8, row: 10 }, Color::White),
|
||||||
|
(Coordinate { column: 9, row: 9 }, Color::White),
|
||||||
|
(Coordinate { column: 10, row: 9 }, Color::Black),
|
||||||
|
(Coordinate { column: 9, row: 8 }, Color::Black),
|
||||||
|
(Coordinate { column: 9, row: 10 }, Color::Black),
|
||||||
|
]
|
||||||
|
.into_iter(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
state.place_stone(Coordinate { column: 8, row: 9 }).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
state.place_stone(Coordinate { column: 9, row: 9 }),
|
||||||
|
Err(BoardError::Ko)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A
|
||||||
|
// 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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn it_calculates_horizontal_position_of_nodes() {
|
||||||
|
let game_tree = branching_tree();
|
||||||
|
let tree = DepthTree::from(&game_tree);
|
||||||
|
|
||||||
|
let node_a = tree.root().unwrap();
|
||||||
|
assert_eq!(node_a.data().position(), (0, 0));
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
let node_c = node_b.first_child().unwrap();
|
||||||
|
assert_eq!(node_c.data().position(), (2, 0));
|
||||||
|
|
||||||
|
let node_d = node_c.first_child().unwrap();
|
||||||
|
assert_eq!(node_d.data().position(), (3, 0));
|
||||||
|
|
||||||
|
let node_i = node_h.first_child().unwrap();
|
||||||
|
assert_eq!(node_i.data().position(), (2, 2));
|
||||||
|
|
||||||
|
/*
|
||||||
|
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));
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue