use crossterm::event::
{
KeyCode,
KeyEvent,
KeyModifiers,
};
use why2_chat::
{
config,
network::codes::{ ServerSetting, SettingValue },
};
#[cfg(feature = "client_voice")]
use why2_chat::network::voice::client::options as voice_options;
#[cfg(feature = "client_screen")]
use why2_chat::network::screen::client::options as screen_options;
use super::state::App;
pub const MAX_PICKER_ROWS: usize = 8;
pub const SAVE_LABEL: &str = "Save"; pub const RESTART_LABEL: &str = "Restart server";
#[cfg(feature = "client_voice")]
pub const DEFAULT_DEVICE: &str = "System default";
#[cfg(feature = "client_voice")]
const VOLUME_STEP: u32 = 5;
pub enum Value
{
Toggle { on: bool, invert: bool },
Number(i64),
Text(String),
#[cfg(feature = "client_voice")]
Volume(u32),
#[cfg(feature = "client_voice")]
Device { id: String, input: bool }, }
pub enum Row
{
Header(String),
Item(Item),
Action(&'static str), }
pub struct Item
{
pub label: String,
pub key: String, pub value: Value,
pub hint: String, pub changed: bool, pub restart: bool, }
#[derive(Clone, Default)]
pub struct DeviceEntry
{
pub id: String,
pub label: String,
}
pub struct Picker {
pub title: &'static str,
pub entries: Vec<DeviceEntry>, pub selected: usize,
pub row: usize, pub offset: usize, }
#[derive(Default)]
pub struct Devices {
#[cfg(feature = "client_voice")]
pub input: Vec<DeviceEntry>,
#[cfg(feature = "client_voice")]
pub output: Vec<DeviceEntry>,
}
pub struct Settings {
pub open: bool,
pub rows: Vec<Row>,
pub selected: usize,
pub picker: Option<Picker>,
pub offset: usize,
pub page: usize,
pub server: bool,
pub edit: Option<String>, pub saving: bool,
save: Option<Vec<ServerSetting>>,
pub restart_note: Option<String>,
pub confirm: bool,
restart: bool,
#[cfg(feature = "client_voice")]
devices: Devices,
}
impl Default for Settings
{
fn default() -> Self { Self::new() }
}
impl Item
{
fn client(label: &str, key: &str, value: Value) -> Self {
Self { label: label.to_string(), key: key.to_string(), value, hint: String::new(), changed: false, restart: false }
}
}
impl Settings
{
pub fn new() -> Self
{
Self
{
open: false,
rows: Vec::new(),
selected: 0,
picker: None,
offset: 0,
page: 0,
server: false,
edit: None,
saving: false,
save: None,
restart_note: None,
confirm: false,
restart: false,
#[cfg(feature = "client_voice")]
devices: Devices::default(),
}
}
pub fn open(&mut self, devices: Devices)
{
let mut rows: Vec<Row> = Vec::new();
#[cfg(feature = "client_voice")]
{
rows.push(Row::Header(String::from("Audio")));
rows.push(Row::Item(Item::client("Input device", "input_device",
Value::Device { id: config::read_config::<String>("input_device"), input: true })));
rows.push(Row::Item(Item::client("Output device", "output_device",
Value::Device { id: config::read_config::<String>("output_device"), input: false })));
rows.push(Row::Item(Item::client("Input volume", "input_volume",
Value::Volume(voice_options::clamp_volume(config::read_config::<u32>("input_volume"))))));
rows.push(Row::Item(Item::client("Output volume", "output_volume",
Value::Volume(voice_options::clamp_volume(config::read_config::<u32>("output_volume"))))));
#[cfg(feature = "client_screen")]
rows.push(Row::Item(Item::client("Screen share volume", "screen_volume",
Value::Volume(voice_options::clamp_volume(config::read_config::<u32>("screen_volume"))))));
rows.push(Row::Item(Item::client("Noise suppression", "noise_suppression",
toggle_value("noise_suppression", false))));
rows.push(Row::Item(Item::client("Automatic gain", "automatic_gain",
toggle_value("automatic_gain", false))));
}
rows.push(Row::Header(String::from("Interface")));
rows.push(Row::Item(Item::client("Message colors", "disable_colors", toggle_value("disable_colors", true))));
rows.push(Row::Item(Item::client("Background logo", "disable_logo", toggle_value("disable_logo", true))));
rows.push(Row::Item(Item::client("Show client IDs", "show_id", toggle_value("show_id", false))));
self.rows = rows;
self.picker = None;
self.open = true;
self.server = false;
self.edit = None;
self.saving = false;
self.selected = 0;
self.offset = 0;
#[cfg(feature = "client_voice")]
{
self.devices = devices;
}
#[cfg(not(feature = "client_voice"))]
let _ = devices;
self.step(1); }
pub fn open_server(&mut self, settings: Vec<ServerSetting>)
{
let mut rows: Vec<Row> = Vec::new();
let mut section = String::new();
for setting in settings
{
if setting.section != section
{
section = setting.section.clone();
if !section.is_empty() { rows.push(Row::Header(section.clone())); }
}
rows.push(Row::Item(Item
{
label: setting.key.replace('_', " "),
key: setting.key,
value: match setting.value
{
SettingValue::Toggle(on) => Value::Toggle { on, invert: false },
SettingValue::Number(number) => Value::Number(number),
SettingValue::Text(text) => Value::Text(text),
},
hint: setting.description,
changed: false,
restart: setting.restart, }));
}
rows.push(Row::Action(SAVE_LABEL)); rows.push(Row::Action(RESTART_LABEL));
self.rows = rows;
self.picker = None;
self.open = true;
self.server = true;
self.edit = None;
self.saving = false;
self.confirm = false;
self.selected = 0;
self.offset = 0;
self.step(1);
}
pub fn close(&mut self)
{
self.open = false;
self.picker = None;
self.edit = None;
self.server = false;
self.saving = false;
self.confirm = false;
self.rows = Vec::new();
self.offset = 0;
self.page = 0;
}
pub fn title(&self) -> String {
if !self.server { return String::from(" Settings "); }
if self.saving { return String::from(" Server settings · saving… "); }
if self.unsaved() { String::from(" Server settings · unsaved ") } else { String::from(" Server settings ") }
}
pub fn unsaved(&self) -> bool {
self.rows.iter().any(|row| matches!(row, Row::Item(item) if item.changed))
}
pub fn take_save(&mut self) -> Option<Vec<ServerSetting>> { self.save.take() }
pub fn take_restart(&mut self) -> bool { std::mem::take(&mut self.restart) }
pub fn stored(&mut self, settings: Vec<ServerSetting>)
{
let selected = self.selected;
let offset = self.offset;
self.open_server(settings);
self.selected = selected.min(self.rows.len().saturating_sub(1));
self.offset = offset;
if matches!(self.rows.get(self.selected), Some(Row::Header(_))) { self.step(1); }
}
fn step(&mut self, delta: isize)
{
if self.rows.is_empty() { return; }
let mut index = self.selected as isize;
loop
{
index += delta;
if index < 0 || index as usize >= self.rows.len() { return; }
if !matches!(self.rows[index as usize], Row::Header(_))
{
self.selected = index as usize;
return;
}
}
}
fn page_move(&mut self, direction: isize)
{
if self.rows.is_empty() { return; }
let page = self.page.max(1) as isize;
self.offset = (self.offset as isize + direction * page).max(0) as usize;
let target = (self.selected as isize + direction * page).clamp(0, self.rows.len() as isize - 1);
self.land(target as usize, direction);
}
fn land(&mut self, index: usize, direction: isize)
{
if self.rows.is_empty() { return; }
self.selected = index.min(self.rows.len() - 1);
if !matches!(self.rows[self.selected], Row::Header(_)) { return; }
let before = self.selected;
self.step(direction);
if self.selected == before { self.step(-direction); }
}
fn first_row(&mut self)
{
self.offset = 0;
self.land(0, 1);
}
fn last_row(&mut self)
{
self.offset = self.rows.len();
self.land(self.rows.len().saturating_sub(1), -1);
}
#[cfg(feature = "client_voice")]
pub fn device_label(&self, id: &str, input: bool) -> String
{
if id.is_empty() { return String::from(DEFAULT_DEVICE); }
let devices = if input { &self.devices.input } else { &self.devices.output };
devices.iter().find(|device| device.id == id).map(|device| device.label.clone()).unwrap_or_else(|| String::from(id))
}
#[cfg(feature = "client_voice")]
pub fn refresh_devices(&mut self)
{
for row in self.rows.iter_mut()
{
let Row::Item(item) = row else { continue };
if let Value::Device { input, .. } = item.value
{
item.value = Value::Device { id: config::read_config::<String>(&item.key), input };
}
}
}
}
fn toggle_value(key: &str, invert: bool) -> Value
{
let stored = config::read_config::<bool>(key);
Value::Toggle { on: if invert { !stored } else { stored }, invert }
}
pub fn handle_key(app: &mut App, key: KeyEvent)
{
if app.settings.picker.is_some()
{
handle_picker_key(app, key);
return;
}
if app.settings.edit.is_some()
{
handle_edit_key(app, key);
return;
}
if !matches!(key.code, KeyCode::Enter | KeyCode::Char(' ')) { app.settings.confirm = false; }
if app.settings.server && key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('s')
{
save(app);
return;
}
match key.code
{
KeyCode::Esc => app.settings.close(),
KeyCode::Up => app.settings.step(-1),
KeyCode::Down => app.settings.step(1),
KeyCode::PageUp => app.settings.page_move(-1),
KeyCode::PageDown => app.settings.page_move(1),
KeyCode::Home => app.settings.first_row(),
KeyCode::End => app.settings.last_row(),
KeyCode::Left => adjust(app, -1),
KeyCode::Right => adjust(app, 1),
KeyCode::Enter | KeyCode::Char(' ') => activate(app),
_ => {},
}
}
pub fn scroll(app: &mut App, delta: isize)
{
if app.settings.edit.is_some() { return; }
match app.settings.picker.as_mut()
{
Some(picker) =>
{
let next = picker.selected as isize + delta;
picker.selected = next.clamp(0, picker.entries.len() as isize - 1) as usize;
},
None => app.settings.step(delta),
}
}
fn handle_picker_key(app: &mut App, key: KeyEvent)
{
match key.code
{
KeyCode::Esc =>
{
app.settings.picker = None;
return;
},
KeyCode::Enter =>
{
let Some(picker) = app.settings.picker.take() else { return };
let chosen = picker.entries.get(picker.selected).map(|entry| entry.id.clone()).unwrap_or_default();
set_device(app, picker.row, chosen);
return;
},
_ => {},
}
let page = app.settings.page.max(1) as isize;
let Some(picker) = app.settings.picker.as_mut() else { return };
match key.code
{
KeyCode::Up => picker.selected = if picker.selected == 0 { picker.entries.len() - 1 } else { picker.selected - 1 },
KeyCode::Down => picker.selected = (picker.selected + 1) % picker.entries.len(),
KeyCode::PageUp | KeyCode::PageDown =>
{
let direction: isize = if key.code == KeyCode::PageUp { -1 } else { 1 };
picker.offset = (picker.offset as isize + direction * page).max(0) as usize;
picker.selected = (picker.selected as isize + direction * page)
.clamp(0, picker.entries.len() as isize - 1) as usize;
},
KeyCode::Home => { picker.selected = 0; picker.offset = 0; },
KeyCode::End => { picker.selected = picker.entries.len() - 1; picker.offset = picker.entries.len(); },
_ => {},
}
}
fn handle_edit_key(app: &mut App, key: KeyEvent)
{
match key.code
{
KeyCode::Esc => app.settings.edit = None,
KeyCode::Enter => commit_edit(app),
KeyCode::Backspace => { if let Some(edit) = app.settings.edit.as_mut() { edit.pop(); } },
KeyCode::Char(c) =>
{
let numeric = matches!(app.settings.rows.get(app.settings.selected), Some(Row::Item(item))
if matches!(item.value, Value::Number(_)));
if let Some(edit) = app.settings.edit.as_mut()
&& (!numeric || c.is_ascii_digit() || (c == '-' && edit.is_empty()))
{
edit.push(c);
}
},
_ => {},
}
}
fn commit_edit(app: &mut App) {
let Some(edit) = app.settings.edit.take() else { return };
let Some(Row::Item(item)) = app.settings.rows.get_mut(app.settings.selected) else { return };
match &item.value
{
Value::Number(current) => match edit.trim().parse::<i64>()
{
Ok(number) if number != *current =>
{
item.value = Value::Number(number);
item.changed = true;
},
_ => {},
},
Value::Text(current) => if edit != *current
{
item.value = Value::Text(edit);
item.changed = true;
},
_ => {},
}
}
enum Selected
{
Toggle(bool),
Number(i64),
Text(String),
Action(&'static str),
#[cfg(feature = "client_voice")]
Volume(String, u32),
#[cfg(feature = "client_voice")]
Device(String, bool),
}
fn selected(app: &App) -> Option<Selected>
{
match app.settings.rows.get(app.settings.selected)?
{
Row::Header(_) => None,
Row::Action(label) => Some(Selected::Action(label)),
Row::Item(item) => Some(match &item.value
{
Value::Toggle { on, .. } => Selected::Toggle(*on),
Value::Number(number) => Selected::Number(*number),
Value::Text(text) => Selected::Text(text.clone()),
#[cfg(feature = "client_voice")]
Value::Volume(percent) => Selected::Volume(item.key.clone(), *percent),
#[cfg(feature = "client_voice")]
Value::Device { id, input } => Selected::Device(id.clone(), *input),
}),
}
}
fn adjust(app: &mut App, direction: i32)
{
let row = app.settings.selected;
match selected(app)
{
Some(Selected::Toggle(on)) => if (direction > 0) != on { toggle(app) },
Some(Selected::Number(number)) =>
{
let next = number.saturating_add(direction as i64);
if let Some(Row::Item(item)) = app.settings.rows.get_mut(row)
{
item.value = Value::Number(next);
item.changed = true;
}
},
Some(Selected::Text(_)) | Some(Selected::Action(_)) => {},
#[cfg(feature = "client_voice")]
Some(Selected::Volume(key, percent)) =>
{
let next = if direction > 0
{
voice_options::clamp_volume(percent.saturating_add(VOLUME_STEP))
} else
{
percent.saturating_sub(VOLUME_STEP)
};
if next == percent { return; }
if let Some(Row::Item(item)) = app.settings.rows.get_mut(row) { item.value = Value::Volume(next); }
config::client_write_int(&key, next as i64);
apply_volume(&key, next);
},
#[cfg(feature = "client_voice")]
Some(Selected::Device(id, input)) =>
{
let entries = device_entries(app, input);
let current = entries.iter().position(|entry| entry.id == id).unwrap_or(0);
let next = (current as isize + direction as isize).rem_euclid(entries.len() as isize) as usize;
let chosen = entries[next].id.clone();
set_device(app, row, chosen);
},
None => {},
}
}
fn activate(app: &mut App)
{
let _row = app.settings.selected;
match selected(app)
{
Some(Selected::Toggle(_)) => toggle(app),
Some(Selected::Number(number)) => app.settings.edit = Some(number.to_string()),
Some(Selected::Text(text)) => app.settings.edit = Some(text),
Some(Selected::Action(RESTART_LABEL)) => restart(app),
Some(Selected::Action(_)) => save(app),
#[cfg(feature = "client_voice")]
Some(Selected::Volume(..)) => adjust(app, 1),
#[cfg(feature = "client_voice")]
Some(Selected::Device(id, input)) =>
{
let entries = device_entries(app, input);
app.settings.picker = Some(Picker
{
title: if input { " Input device " } else { " Output device " },
selected: entries.iter().position(|entry| entry.id == id).unwrap_or(0),
entries,
row: _row,
offset: 0,
});
},
None => {},
}
}
fn restart(app: &mut App)
{
if !app.settings.server || app.settings.saving || app.settings.unsaved() { return; }
if !app.settings.confirm
{
app.settings.confirm = true;
return;
}
app.settings.restart = true;
app.settings.close();
}
fn save(app: &mut App)
{
if !app.settings.server { return; }
let changed: Vec<ServerSetting> = app.settings.rows.iter().filter_map(|row|
{
let Row::Item(item) = row else { return None };
if !item.changed { return None; }
Some(ServerSetting
{
key: item.key.clone(),
value: match &item.value
{
Value::Toggle { on, .. } => SettingValue::Toggle(*on),
Value::Number(number) => SettingValue::Number(*number),
Value::Text(text) => SettingValue::Text(text.clone()),
#[allow(unreachable_patterns)] _ => return None,
},
section: String::new(),
description: String::new(),
restart: false,
})
}).collect();
if changed.is_empty() { return; }
let restart: Vec<&str> = app.settings.rows.iter().filter_map(|row| match row
{
Row::Item(item) if item.changed && item.restart => Some(item.key.as_str()),
_ => None,
}).collect();
app.settings.restart_note = (!restart.is_empty()).then(|| restart.join(", "));
app.settings.saving = true;
app.settings.save = Some(changed);
}
fn toggle(app: &mut App)
{
let server = app.settings.server;
let changed = match app.settings.rows.get_mut(app.settings.selected)
{
Some(Row::Item(item)) => match &item.value
{
Value::Toggle { on, invert } =>
{
let (next, invert) = (!*on, *invert);
item.value = Value::Toggle { on: next, invert };
item.changed = item.changed || server;
Some((item.key.clone(), next, invert))
},
#[allow(unreachable_patterns)] _ => None,
},
_ => None,
};
let Some((key, next, invert)) = changed else { return };
if server { return; }
config::client_write_bool(&key, if invert { !next } else { next });
app.reload_theme();
#[cfg(feature = "client_voice")]
match key.as_str()
{
"noise_suppression" => voice_options::set_noise_suppression(next),
"automatic_gain" => voice_options::set_automatic_gain(next),
_ => {},
}
}
#[cfg(feature = "client_voice")]
fn set_device(app: &mut App, row: usize, chosen: String)
{
let Some(Row::Item(item)) = app.settings.rows.get_mut(row) else { return };
let Value::Device { id, input } = &item.value else { return };
if *id == chosen { return; }
let (key, input) = (item.key.clone(), *input);
item.value = Value::Device { id: chosen.clone(), input };
config::client_write(&key, &chosen);
voice_options::mark_devices_changed();
}
#[cfg(not(feature = "client_voice"))]
fn set_device(_app: &mut App, _row: usize, _chosen: String) {}
#[cfg(feature = "client_voice")]
fn device_entries(app: &App, input: bool) -> Vec<DeviceEntry>
{
let devices = if input { &app.settings.devices.input } else { &app.settings.devices.output };
let mut entries = vec![DeviceEntry { id: String::new(), label: String::from(DEFAULT_DEVICE) }];
entries.extend(devices.iter().cloned());
let configured = config::read_config::<String>(if input { "input_device" } else { "output_device" });
if !configured.is_empty() && !entries.iter().any(|entry| entry.id == configured)
{
entries.push(DeviceEntry { label: configured.clone(), id: configured });
}
entries
}
#[cfg(feature = "client_voice")]
fn apply_volume(key: &str, percent: u32) {
match key
{
"input_volume" => voice_options::set_input_volume(percent),
"output_volume" => voice_options::set_output_volume(percent),
#[cfg(feature = "client_screen")]
"screen_volume" => screen_options::set_screen_volume(percent),
_ => {},
}
}