[Add] basic split

This commit is contained in:
Alex Emmet 2026-04-04 19:46:21 +02:00
commit 3cdf7c62d5
77 changed files with 506 additions and 648 deletions

17
iota-core/Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "iota-core"
version = "0.1.0"
edition = "2024"
[dependencies]
iota-state = { path = "../iota-state" }
ttp-core = { git = "https://github.com/Tensamin/TTP.git", package = "ttp-core" }
ttp-native = { git = "https://github.com/Tensamin/TTP.git", package = "ttp-native" }
dashmap = "6.1.0"
json = "*"
once_cell = "1.21.3"
pnet = "0.35.0"
ratatui = "0.30.0"
reqwest = "0.13.2"
tokio = { version = "1.50.0", features = ["full"] }

151
iota-core/src/main.rs Normal file
View file

@ -0,0 +1,151 @@
use pnet::datalink::NetworkInterface;
use tokio::time::{Duration, sleep};
use iota_state::{ACTIVE_TASKS, APP_STATE, AppState, RELOAD, SHUTDOWN};
#[tokio::main(flavor = "multi_thread", worker_threads = 16)]
#[allow(unused_must_use, dead_code)]
async fn main() {
while *RELOAD.read().await {
*RELOAD.write().await = false;
*SHUTDOWN.write().await = false;
let ui = start_tui();
let (eula, tos_pp) = consent_state::check(ui.clone()).await;
if !eula {
*SHUTDOWN.write().await = true;
loop {
if ACTIVE_TASKS.is_empty() {
break;
}
sleep(Duration::from_millis(100)).await;
}
println!("You need to accept our End User Licence Agreement before launching!");
println!("You can find this at 'agreements'!");
return;
}
if !tos_pp {
*SHUTDOWN.write().await = true;
loop {
if ACTIVE_TASKS.is_empty() {
break;
}
sleep(Duration::from_millis(100)).await;
}
println!(
"Please accept our Privacy Policy & Terms of Serivce before using Tensamin Services!"
);
println!("In future releases this will be optional!");
println!("You can find this at 'agreements'!");
return;
}
iota_state::setup();
let main_screen = MainScreen::new(ui.clone()).await;
ui.set_screen(Box::new(main_screen)).await;
// LANGUAGE PACK
if let Err(e) = language_creator::create_languages() {
println!("Language pack creation failed: {}", e);
return;
}
// UI
logger::startup();
// BASIC CONFIGURATION
&CONFIG.write().await.load();
// USER MANAGEMENT
if let Err(_) = user_manager::load_users().await {
log_t!("user_load_failed");
}
let mut sb = "".to_string();
for up in user_manager::get_users() {
sb = sb + "," + &up.user_id.to_string().as_str();
}
if !sb.is_empty() {
{}
sb.remove(0);
sb = sb + ",";
}
log!(
"IOTA ID: {}",
CONFIG.read().await.get_iota_id().to_string()
);
log!("User IDS: {}", sb);
// COMMUNITY MANAGEMENT
/* registry::load_interactables().await;
community_manager::load_communities().await;
community_manager::save_communities().await;
let mut sb1 = "".to_string();
for cp in community_manager::get_communities().await {
sb1 = sb1 + "," + &cp.get_name().to_string().as_str();
}
if !sb1.is_empty() {
sb1.remove(0);
sb1 = sb1 + ",";
}
log!("Community IDS: {}", sb1); */
let port = CONFIG.read().await.get_port();
let mut ip = "0.0.0.0".to_string();
for iface in pnet::datalink::interfaces() {
let iface: NetworkInterface = iface;
if iface.ips.len() > 0 {
let ipsv = format!("{}", iface.ips[0]);
let ips: &str = ipsv.split('/').next().unwrap_or("");
if format!("{}", ips).starts_with("10.") || format!("{}", ips).starts_with("192.") {
ip = ips.to_string();
}
}
}
/*
if start(port).await {
log_t!("community_active", ip, port.to_string());
} else {
if port < 1024 {
log_t!("community_start_error_admin", port.to_string());
} else {
log_t!("community_start_error", port.to_string());
}
} */
if !has_dir("web") {
download_and_extract_zip(
"https://omega.tensamin.net/api/download/iota_frontend",
"web",
)
.await;
}
let _ = omikron::omikron_connection::get_omikron_connection().await;
log_t!("setup_completed");
loop {
if *SHUTDOWN.read().await {
break;
}
sleep(Duration::from_millis(100)).await;
}
if *RELOAD.read().await {
loop {
if ACTIVE_TASKS.is_empty() {
break;
}
sleep(Duration::from_secs(1)).await;
}
&CONFIG.write().await.clear();
user_manager::clear();
/*community_manager::clear();*/
*APP_STATE.lock().unwrap() = AppState::new();
}
ui.terminal.lock().unwrap().clear();
ui.terminal.lock().unwrap().flush();
}
}

View file

@ -0,0 +1,175 @@
use ratatui::{
layout::{Alignment, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
use crate::terms::focus::Focus;
#[allow(mismatched_lifetime_syntaxes)]
pub fn checkbox(label: &str, checked: bool, active: bool, allowed: bool) -> Line {
let box_char = if checked { "[x]" } else { "[ ]" };
let (box_style, text_style) = if active {
if allowed {
(
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD),
)
} else {
(
Style::default().fg(Color::Gray),
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
)
}
} else {
(Style::default(), Style::default())
};
Line::from(vec![
Span::styled(box_char, box_style),
Span::raw(" "),
Span::styled(label, text_style),
])
}
pub fn draw_button(f: &mut ratatui::Frame, area: Rect, label: &str, style: Style) {
let p = Paragraph::new(Span::styled(label, style))
.alignment(Alignment::Center)
.block(Block::default().borders(Borders::ALL));
f.render_widget(p, area);
}
pub fn draw_buttons(
f: &mut ratatui::Frame,
area: Rect,
current_focus: Focus,
state: (bool, bool),
update_needed: bool,
downgrade_scenario: bool,
tos_or_privacy: bool,
) {
let cancel_text = if update_needed {
"[Q] Quit"
} else {
"[Q] Not now"
};
let continue_text = if downgrade_scenario {
"Downgrade"
} else {
"Continue"
};
let mut buttons = vec![
(cancel_text, Focus::Cancel),
(continue_text, Focus::Continue),
];
if tos_or_privacy {
buttons.push(("Continue with Tensamin Services", Focus::ContinueAll));
}
let padding = 2;
let min_widths: Vec<u16> = buttons
.iter()
.map(|(label, _)| label.len() as u16 + padding)
.collect();
let widths = compute_widths(area.width, &min_widths);
let mut x = area.x;
for ((label, focus), width) in buttons.iter().zip(widths) {
let chunk = Rect {
x,
y: area.y,
width,
height: area.height,
};
x += width;
let is_focused = current_focus == *focus;
let style = match focus {
Focus::Cancel => {
if is_focused {
Style::default()
.fg(Color::Black)
.bg(Color::Red)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Red)
}
}
Focus::Continue => {
if is_focused && state.0 {
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD)
} else if state.0 {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::DarkGray)
}
}
Focus::ContinueAll => {
if is_focused && state.1 {
Style::default()
.fg(Color::Black)
.bg(Color::Green)
.add_modifier(Modifier::BOLD)
} else if state.1 {
Style::default().fg(Color::Green)
} else {
Style::default().fg(Color::DarkGray)
}
}
_ => Style::default().fg(Color::DarkGray),
};
draw_button(f, chunk, label, style);
}
}
pub fn compute_widths(area_width: u16, min_widths: &[u16]) -> Vec<u16> {
let mut widths = vec![0; min_widths.len()];
let mut remaining: Vec<usize> = (0..min_widths.len()).collect();
let mut remaining_width = area_width;
while !remaining.is_empty() {
let count = remaining.len() as u16;
let equal = remaining_width / count;
let mut clamped = Vec::new();
for &i in &remaining {
if min_widths[i] > equal {
widths[i] = min_widths[i];
remaining_width -= min_widths[i];
clamped.push(i);
}
}
if clamped.is_empty() {
let mut remainder = remaining_width % count;
for &i in &remaining {
widths[i] = equal
+ if remainder > 0 {
remainder -= 1;
1
} else {
0
};
}
break;
}
remaining.retain(|i| !clamped.contains(i));
}
widths
}

View file

@ -0,0 +1,73 @@
use json::{JsonValue, object::Object};
use crate::{terms::terms_getter::Type, util::file_util::load_file};
#[derive(Clone, Debug, PartialEq, Eq)]
#[allow(unused)]
pub struct Doc {
version: String,
hash: String,
pub doc_type: Type,
timestamp: u64,
}
#[allow(dead_code)]
impl Doc {
pub fn new(version: String, hash: String, doc_type: Type, timestamp: u64) -> Doc {
Doc {
version,
hash,
doc_type,
timestamp,
}
}
pub fn equals_some(&self, other: &Option<Self>) -> bool {
if let Some(other) = other {
self.get_version() == other.get_version() && self.get_hash() == other.get_hash()
} else {
false
}
}
pub fn equals(&self, other: &Self) -> bool {
self.get_version() == other.get_version() && self.get_hash() == other.get_hash()
}
pub fn get_version(&self) -> String {
self.version.clone()
}
pub fn get_hash(&self) -> String {
self.hash.clone()
}
pub fn get_time(&self) -> u64 {
self.timestamp.clone()
}
pub fn get_content(&self) -> String {
load_file(
format!("docs/{}/", self.doc_type.to_str()).as_str(),
format!("{}.md", self.version).as_str(),
)
}
pub fn to_json(&self) -> JsonValue {
let mut json = JsonValue::new_object();
let _ = json.insert("version", self.version.clone());
let _ = json.insert("hash", self.hash.clone());
let _ = json.insert("unix", self.timestamp.clone());
json
}
pub fn from_json(doc_type: Type, json: Object) -> Option<Self> {
let hash = json.get("hash")?.as_str()?.to_string();
let version = json.get("version")?.as_str()?.to_string();
let timestamp = json.get("unix")?.as_u64()?;
Some(Doc {
version,
hash,
doc_type,
timestamp,
})
}
}

View file

@ -0,0 +1,25 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Focus {
Eula,
Tos,
Pp,
Cancel,
Continue,
ContinueAll,
}
impl Focus {
pub fn next(&mut self, order: &[Focus]) {
if let Some(pos) = order.iter().position(|&f| f == *self) {
let next_pos = (pos + 1) % order.len();
*self = order[next_pos];
}
}
pub fn prev(&mut self, order: &[Focus]) {
if let Some(pos) = order.iter().position(|&f| f == *self) {
let prev_pos = if pos == 0 { order.len() - 1 } else { pos - 1 };
*self = order[prev_pos];
}
}
}

View file

@ -0,0 +1,4 @@
pub mod buttons;
pub mod doc;
pub mod focus;
pub mod terms_getter;

View file

@ -0,0 +1,105 @@
use json::JsonValue::Object;
use crate::terms::doc::Doc;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Type {
EULA,
TOS,
PP,
}
impl Type {
pub fn to_str(&self) -> &str {
match self {
Self::EULA => "eula",
Self::TOS => "tos",
Self::PP => "privacy",
}
}
pub fn to_string(&self) -> String {
match self {
Self::EULA => "End User License Agreement".to_string(),
Self::TOS => "Terms of Service".to_string(),
Self::PP => "Privacy Policy".to_string(),
}
}
}
pub fn get_link(terms_type: Type) -> String {
format!("https://legal.tensamin.net/{}/", terms_type.to_str())
}
pub fn get_newest_link(terms_type: Type) -> String {
format!("https://legal.tensamin.net/{}/newest/", terms_type.to_str())
}
pub async fn get_current_docs() -> Option<(Doc, Doc, Doc)> {
let body = reqwest::get("https://legal.tensamin.net/api/current/")
.await
.ok()?
.text()
.await
.ok()?;
let json = json::parse(&body).ok()?;
if let Object(eula) = &json["eula"] {
if let Object(tos) = &json["tos"] {
if let Object(pp) = &json["pp"] {
Some((
Doc::from_json(Type::EULA, eula.clone())?,
Doc::from_json(Type::TOS, tos.clone())?,
Doc::from_json(Type::PP, pp.clone())?,
))
} else {
None
}
} else {
None
}
} else {
None
}
}
pub async fn get_newest_docs() -> Option<(Doc, Doc, Doc)> {
let body = reqwest::get("https://legal.tensamin.net/api/newest/")
.await
.ok()?
.text()
.await
.ok()?;
let json = json::parse(&body).ok()?;
if let Object(eula) = &json["eula"] {
if let Object(tos) = &json["tos"] {
if let Object(pp) = &json["pp"] {
Some((
Doc::from_json(Type::EULA, eula.clone())?,
Doc::from_json(Type::TOS, tos.clone())?,
Doc::from_json(Type::PP, pp.clone())?,
))
} else {
None
}
} else {
None
}
} else {
None
}
}
pub async fn get_terms(terms_type: Type) -> Option<String> {
let body = reqwest::get(format!(
"https://legal.tensamin.net/api/text/{}/",
terms_type.to_str()
))
.await
.ok()?
.text()
.await
.ok()?;
Some(body)
}