Compare commits

...

5 Commits

Author SHA1 Message Date
mars cabb4a9fa6 Improve main chat UI 2022-05-29 18:46:45 -06:00
mars 46b8374de2 Remove dummy pronoun format string 2022-05-29 18:28:22 -06:00
mars 3f79394186 Add plural example usage 2022-05-28 18:11:59 -06:00
mars 97c2c6c070 VERY hacky message sending 2022-05-27 00:00:56 -06:00
mars a4901bf136 Merge cursive and udp loops 2022-05-26 23:11:24 -06:00
3 changed files with 352 additions and 329 deletions

View File

@ -1,4 +1,6 @@
use clap::Parser;
use crossbeam_channel::{Receiver, Sender};
use cursive::Cursive;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use protocol::*;
use protocol_derive::{Decode, Encode};
@ -73,21 +75,34 @@ pub struct Room {
pub struct App {
args: Args,
tui: tui::Tui,
socket: UdpSocket,
cursive: Cursive,
owned_rooms: HashMap<String, Room>,
remote_rooms: HashMap<String, Room>,
message_sender: Sender<String>,
message_receiver: Receiver<String>,
// TODO connection management
other: Option<SocketAddr>,
}
impl App {
pub fn new(args: Args) -> Self {
let socket = UdpSocket::bind(args.bind_addr).unwrap();
socket.set_nonblocking(true).unwrap();
let (message_sender, message_receiver) = crossbeam_channel::unbounded();
let cursive = tui::make_cursive(message_sender.to_owned());
let mut app = Self {
args,
tui: tui::Tui::new(),
socket,
cursive,
owned_rooms: Default::default(),
remote_rooms: Default::default(),
socket,
message_sender,
message_receiver,
other: None,
};
app.startup();
app
@ -111,28 +126,56 @@ impl App {
}
pub fn run(mut self) {
self.tui.run();
let mut siv = tui::make_cursive(self.message_sender.to_owned());
let siv_backend = cursive::backends::try_default().unwrap();
let mut siv_runner = siv.runner(siv_backend);
siv_runner.refresh();
let mut buf = [0u8; 65507];
while let Ok((len, from)) = self.socket.recv_from(&mut buf) {
let mut buf = buf.as_slice();
println!("recv'd from {:?}: {:?}", from, &buf[..len]);
while siv_runner.is_running() {
siv_runner.step();
let kind = Var::<u16>::decode(&mut buf).unwrap().0;
let kind: PacketKind = match kind.try_into() {
Ok(kind) => kind,
Err(int) => {
eprintln!("unrecognized packet kind {}", int);
continue;
let mut buf = [0u8; 65507];
// TODO error handling of non-non-blocking errors
if let Ok((len, from)) = self.socket.recv_from(&mut buf) {
let mut buf = buf.as_slice();
let kind = Var::<u16>::decode(&mut buf).unwrap().0;
let kind: PacketKind = match kind.try_into() {
Ok(kind) => kind,
Err(int) => {
eprintln!("unrecognized packet kind {}", int);
continue;
}
};
if let Some(message) = self.on_packet(from, kind, buf) {
tui::add_message(&mut siv_runner, &message);
siv_runner.refresh(); // TODO better refresh management
}
};
}
self.on_packet(from, kind, buf);
if let Some(other) = self.other.as_ref() {
while let Ok(message) = self.message_receiver.try_recv() {
eprintln!("sending message: {}", message);
self.send_packet(other, PacketKind::Message, |writer| {
let message = Message {
sender: self.args.username.clone(),
contents: message,
};
message.encode(writer)
})
.unwrap();
}
}
}
}
pub fn on_packet(&mut self, from: SocketAddr, kind: PacketKind, mut reader: &[u8]) {
pub fn on_packet(&mut self, from: SocketAddr, kind: PacketKind, mut reader: &[u8]) -> Option<Message> {
println!("handling {:?}", kind);
// TODO proper connection management
self.other = Some(from);
match kind {
PacketKind::Ping => self.send_empty_packet(from, PacketKind::Pong).unwrap(),
PacketKind::Pong => self
@ -169,8 +212,14 @@ impl App {
eprintln!("Received room info: {:#?}", info);
self.remote_rooms.insert(info.id.clone(), Room { info });
}
PacketKind::Message => {
let message = Message::decode(&mut reader).unwrap();
return Some(message);
}
kind => eprintln!("unimplemented packet handler for {:?}", kind),
}
None
}
pub fn build_room_list(&self) -> RoomList {

View File

@ -5,7 +5,8 @@ pub const EXAMPLE_USAGE: &str = "{S} went to the park.
I went with {o}.
{S} brought {p} frisbee.
At least I think it was {pp}.
{S} threw the frisbee to {r}.";
{S} threw the frisbee to {r}.
{S} {{if pl}}are{{else}}is{{endif}} happy.";
pub fn make_presets() -> Vec<Pronouns> {
// TODO add more from https://pronoun.is and https://askanonbinary.tumblr.com/pronouns
@ -13,6 +14,7 @@ pub fn make_presets() -> Vec<Pronouns> {
(false, false, "she", "her", "her", "hers", "herself"),
(false, false, "he", "him", "his", "his", "himself"),
(false, true, "they", "them", "their", "theirs", "themselves"),
(false, true, "they", "them", "their", "theirs", "themself"),
(false, false, "fae", "faer", "faer", "faers", "faerself"),
(false, false, "e", "em", "eir", "eirs", "emself"),
(true, false, "E", "Em", "Eir", "Eirs", "Emself"),
@ -146,7 +148,10 @@ impl Pronouns {
/// Baked pronoun lookup table for formatting messages.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PronounTable {
#[serde(rename = "cs")]
pub case_sensitive: bool,
#[serde(rename = "pl")]
pub plural: bool,
#[serde(rename = "s")]

View File

@ -1,338 +1,307 @@
use crate::pronouns::Pronouns;
use crossbeam_channel::{Receiver, Sender};
use crossbeam_channel::Sender;
use cursive::align::*;
use cursive::event::{Event, Key};
use cursive::theme::*;
use cursive::traits::*;
use cursive::view::*;
use cursive::views::*;
use cursive::{Cursive, CursiveExt};
use cursive::Cursive;
pub enum EditEvent {
Name(String),
About(String),
Pronouns(Option<Pronouns>),
pub fn make_cursive(message_sender: Sender<String>) -> Cursive {
let mut cursive = Cursive::new();
cursive.set_user_data(message_sender);
cursive.update_theme(|theme| {
theme.shadow = false;
theme.borders = BorderStyle::Simple;
let palette = &mut theme.palette;
palette[PaletteColor::Background] = Color::TerminalDefault;
palette[PaletteColor::View] = Color::TerminalDefault;
palette[PaletteColor::Primary] = Color::TerminalDefault;
});
cursive.add_global_callback(Event::Char('h'), |siv| siv.on_event(Event::Key(Key::Left)));
cursive.add_global_callback(Event::Char('j'), |siv| siv.on_event(Event::Key(Key::Down)));
cursive.add_global_callback(Event::Char('k'), |siv| siv.on_event(Event::Key(Key::Up)));
cursive.add_global_callback(Event::Char('l'), |siv| siv.on_event(Event::Key(Key::Right)));
show_welcome_mat(&mut cursive);
cursive
}
pub struct Tui {
cursive: Cursive,
edit_receiver: Receiver<EditEvent>,
identity: Identity,
}
pub fn add_message(siv: &mut Cursive, message: &crate::Message) {
siv.call_on_name("messages_list", |messages: &mut LinearLayout| {
let text = format!("{:<16}{}", message.sender, message.contents);
let mut text = TextView::new(text);
text.set_content_wrap(true);
messages.add_child(text);
});
impl Tui {
pub fn new() -> Self {
let (edit_sender, edit_receiver) = crossbeam_channel::unbounded();
let mut cursive = Cursive::new();
cursive.set_user_data(edit_sender);
cursive.update_theme(|theme| {
theme.shadow = false;
theme.borders = BorderStyle::Simple;
let palette = &mut theme.palette;
palette[PaletteColor::Background] = Color::TerminalDefault;
palette[PaletteColor::View] = Color::TerminalDefault;
palette[PaletteColor::Primary] = Color::TerminalDefault;
});
cursive.add_global_callback(Event::Char('h'), |siv| siv.on_event(Event::Key(Key::Left)));
cursive.add_global_callback(Event::Char('j'), |siv| siv.on_event(Event::Key(Key::Down)));
cursive.add_global_callback(Event::Char('k'), |siv| siv.on_event(Event::Key(Key::Up)));
cursive.add_global_callback(Event::Char('l'), |siv| siv.on_event(Event::Key(Key::Right)));
Self {
cursive,
edit_receiver,
identity: Identity {
name: "<display name>".into(),
about: "<short about text>".into(),
pronouns: None,
},
}
}
pub fn run(&mut self) {
/*Self::edit_identity(&mut self.cursive);
self.cursive.run();
while let Ok(edit) = self.edit_receiver.try_recv() {
match edit {
EditEvent::Name(name) => self.identity.name = name,
EditEvent::About(about) => self.identity.about = about,
EditEvent::Pronouns(pronouns) => self.identity.pronouns = pronouns,
siv.call_on_name(
"messages_list_scroll",
|scroll: &mut ScrollView<NamedView<LinearLayout>>| {
if scroll.is_at_bottom() {
scroll.set_scroll_strategy(ScrollStrategy::StickToBottom);
}
}
},
);
}
Self::show_main(&mut self.cursive);
self.cursive.run();*/
pub fn show_welcome_mat(siv: &mut Cursive) {
let logo = TextView::new(include_str!("logo.txt")).center();
let labels = TextView::new("Identity:");
let values = Button::new_raw("<none>", |siv| edit_identity(siv));
let config = LinearLayout::horizontal().child(labels).child(values);
let layout = LinearLayout::vertical().child(logo).child(config);
let dialog = Dialog::around(layout)
.title("Welcome User!")
.button("Link start!", |siv| show_main(siv))
.button("Quit", Cursive::quit);
siv.add_layer(dialog);
}
Self::show_welcome_mat(&mut self.cursive);
self.cursive.run();
}
pub fn edit_identity(siv: &mut Cursive) {
let labels = make_vertical_labels(&["Name:", "About:", "Pronouns:"]).fixed_width(10);
pub fn show_welcome_mat(siv: &mut Cursive) {
let logo = TextView::new(include_str!("logo.txt")).center();
let labels = TextView::new("Identity:");
let values = Button::new_raw("<none>", |siv| Self::edit_identity(siv));
let config = LinearLayout::horizontal().child(labels).child(values);
let layout = LinearLayout::vertical().child(logo).child(config);
let dialog = Dialog::around(layout)
.title("Welcome User!")
.button("Link start!", |siv| Self::show_main(siv))
.button("Quit", Cursive::quit);
siv.add_layer(dialog);
}
let values = LinearLayout::vertical()
.child(EditView::new().with_name("name_edit"))
.child(EditView::new().with_name("about_edit"))
.child(TextView::new("<none>").with_name("pronouns_text"))
.fixed_width(45);
pub fn edit_identity(siv: &mut Cursive) {
let labels = make_vertical_labels(&["Name:", "About:", "Pronouns:"]).fixed_width(10);
let columns = LinearLayout::horizontal().child(labels).child(values);
let dialog = Dialog::around(columns)
.title("Edit Identity")
.button("Select Pronouns...", |siv| select_pronouns(siv))
.dismiss_button("Ok");
let values = LinearLayout::vertical()
.child(EditView::new().with_name("name_edit"))
.child(EditView::new().with_name("about_edit"))
.child(TextView::new("<none>").with_name("pronouns_text"))
.fixed_width(45);
siv.add_layer(dialog);
}
let columns = LinearLayout::horizontal().child(labels).child(values);
let mut dialog = Dialog::around(columns);
dialog.set_title("Edit Identity");
dialog.add_button("Select Pronouns...", |siv| Self::select_pronouns(siv));
dialog.add_button("Ok", |siv| {
let name = get_edit_contents(siv, "name_edit");
let about = get_edit_contents(siv, "about_edit");
siv.with_user_data(|sender: &mut Sender<EditEvent>| {
sender.send(EditEvent::Name(name)).unwrap();
sender.send(EditEvent::About(about)).unwrap();
});
fn make_example_usage_panel() -> impl View {
let text = TextView::new("Highlight a pronoun set to preview its usage!")
.with_name("pronoun_example_text")
.fixed_width(50)
.scrollable();
Panel::new(text).title("Example Usage")
}
fn update_pronouns_edit(siv: &mut Cursive, pronouns: &Pronouns) {
siv.call_on_name("pronoun_example_text", |view: &mut TextView| {
view.set_content(pronouns.make_example_usage());
});
siv.call_on_name("pronouns_text", |view: &mut TextView| {
view.set_content(pronouns.format_full());
})
.unwrap();
}
pub fn select_pronouns(siv: &mut Cursive) {
let example = TextView::new("Highlight a pronoun set to preview its usage!")
.with_name("pronoun_example_text")
.fixed_width(35)
.scrollable();
let presets = SelectView::new()
.with_all(
crate::pronouns::make_presets()
.into_iter()
.map(|pronouns| (pronouns.format_full(), pronouns)),
)
.on_select(|siv, pronouns| update_pronouns_edit(siv, pronouns))
.scrollable();
let layout = LinearLayout::horizontal()
.child(Panel::new(presets).title("Presets"))
.child(Panel::new(example).title("Example Usage"));
let dialog = Dialog::around(layout)
.title("Select Pronouns")
.button("Custom...", |siv| {
siv.pop_layer();
edit_pronouns(siv);
})
.dismiss_button("Ok")
.button("None", |siv| {
siv.call_on_name("pronouns_text", |view: &mut TextView| {
view.set_content("<none>");
})
.unwrap();
siv.pop_layer();
});
siv.add_layer(dialog);
}
siv.add_layer(dialog);
}
fn get_edit_pronouns(siv: &mut Cursive) -> Pronouns {
let case_sensitive = get_checkbox_contents(siv, "case_sensitive_edit");
let plural = get_checkbox_contents(siv, "plural_edit");
let subject = get_edit_contents(siv, "subject_edit");
let object = get_edit_contents(siv, "object_edit");
let possessive = get_edit_contents(siv, "possessive_edit");
let possessive_pronoun = get_edit_contents(siv, "possessive_pronoun_edit");
let reflexive = get_edit_contents(siv, "reflexive_edit");
fn make_example_usage_panel() -> impl View {
let text = TextView::new("Highlight a pronoun set to preview its usage!")
.with_name("pronoun_example_text")
.fixed_width(50)
.scrollable();
Panel::new(text).title("Example Usage")
}
fn update_pronouns_edit(siv: &mut Cursive, pronouns: &Pronouns) {
siv.call_on_name("pronoun_example_text", |view: &mut TextView| {
view.set_content(pronouns.make_example_usage());
});
siv.with_user_data(|sender: &mut Sender<EditEvent>| {
sender
.send(EditEvent::Pronouns(Some(pronouns.clone())))
.unwrap();
});
siv.call_on_name("pronouns_text", |view: &mut TextView| {
view.set_content(pronouns.format_full());
})
.unwrap();
}
pub fn select_pronouns(siv: &mut Cursive) {
let example = TextView::new("Highlight a pronoun set to preview its usage!")
.with_name("pronoun_example_text")
.fixed_width(35)
.scrollable();
let presets = SelectView::new()
.with_all(
crate::pronouns::make_presets()
.into_iter()
.map(|pronouns| (pronouns.format_full(), pronouns)),
)
.on_select(|siv, pronouns| Self::update_pronouns_edit(siv, pronouns))
.scrollable();
let layout = LinearLayout::horizontal()
.child(Panel::new(presets).title("Presets"))
.child(Panel::new(example).title("Example Usage"));
let dialog = Dialog::around(layout)
.title("Select Pronouns")
.button("Custom...", |siv| {
siv.pop_layer();
Self::edit_pronouns(siv);
})
.dismiss_button("Ok")
.button("None", |siv| {
siv.with_user_data(|sender: &mut Sender<EditEvent>| {
sender.send(EditEvent::Pronouns(None)).unwrap();
});
siv.call_on_name("pronouns_text", |view: &mut TextView| {
view.set_content("<none>");
})
.unwrap();
siv.pop_layer();
});
siv.add_layer(dialog);
}
fn get_edit_pronouns(siv: &mut Cursive) -> Pronouns {
let case_sensitive = get_checkbox_contents(siv, "case_sensitive_edit");
let plural = get_checkbox_contents(siv, "plural_edit");
let subject = get_edit_contents(siv, "subject_edit");
let object = get_edit_contents(siv, "object_edit");
let possessive = get_edit_contents(siv, "possessive_edit");
let possessive_pronoun = get_edit_contents(siv, "possessive_pronoun_edit");
let reflexive = get_edit_contents(siv, "reflexive_edit");
Pronouns {
case_sensitive,
plural,
subject,
object,
possessive,
possessive_pronoun,
reflexive,
}
}
pub fn edit_pronouns(siv: &mut Cursive) {
let labels = make_vertical_labels(&[
"Case-sensitive:",
"Plural:",
"Subject:",
"Object:",
"Possessive:",
"Possessive pronoun:",
"Reflexive:",
])
.fixed_width(20);
let mut values = LinearLayout::vertical();
let checkboxes = &["case_sensitive_edit", "plural_edit"];
for name in checkboxes.iter() {
values.add_child(
Checkbox::new()
.on_change(|siv, _value| {
let pronouns = Self::get_edit_pronouns(siv);
Self::update_pronouns_edit(siv, &pronouns);
})
.with_name(*name),
);
}
let edit_views = &[
"subject_edit",
"object_edit",
"possessive_edit",
"possessive_pronoun_edit",
"reflexive_edit",
];
for name in edit_views.iter() {
values.add_child(
EditView::new()
.on_edit(|siv, _text, _cursor| {
let pronouns = Self::get_edit_pronouns(siv);
Self::update_pronouns_edit(siv, &pronouns);
})
.with_name(*name),
);
}
let edit_layout = Panel::new(
LinearLayout::horizontal()
.child(labels)
.child(values.fixed_width(12)),
);
let example = Self::make_example_usage_panel();
let layout = LinearLayout::horizontal().child(edit_layout).child(example);
let dialog = Dialog::around(layout)
.title("Edit Pronouns")
.button("Ok", |siv| {
let pronouns = Self::get_edit_pronouns(siv);
Self::update_pronouns_edit(siv, &pronouns);
siv.pop_layer();
})
.dismiss_button("Cancel");
siv.add_layer(dialog);
}
pub fn show_main(siv: &mut Cursive) {
let messages = LinearLayout::vertical()
.child(TextView::new("Hello, world!"))
.with_name("messages_list")
.scrollable()
.full_height();
let message_edit = EditView::new()
.on_submit(|siv, text| {
siv.call_on_name("message_edit", |message: &mut EditView| {
message.set_content("");
});
siv.call_on_name("messages_list", |messages: &mut LinearLayout| {
messages.add_child(TextView::new(text));
});
})
.with_name("message_edit")
.full_width();
let message_edit = Panel::new(message_edit)
.title("Send Message")
.title_position(HAlign::Left);
let upload_button = Button::new("Upload", |siv| {
siv.add_layer(Dialog::info("Uploading system goes here"))
});
let message_buttons = Panel::new(upload_button);
let message_bar = LinearLayout::horizontal()
.child(message_edit)
.child(message_buttons);
let chat = LinearLayout::vertical().child(messages).child(message_bar);
let chat = Panel::new(chat).title("Chat");
let mut rooms = SelectView::<&'static str>::new().on_submit(|siv, room: &str| {
let dialog = Dialog::info(format!("Selected room: {}", room));
siv.add_layer(dialog);
});
rooms.add_item("Room 1", "room_id_1");
rooms.add_item("Room 2", "room_id_2");
rooms.add_item("Room 3", "room_id_3");
let rooms = Dialog::around(rooms)
.button("Create...", |siv| {
siv.add_layer(Dialog::info("Room creation dialog goes here"))
})
.title("Rooms")
.title_position(HAlign::Left)
.with_name("room_select");
let mut connections = SelectView::new();
connections.add_item("Connection 1", "connection_1");
let connections = Dialog::around(connections)
.title("Connections")
.title_position(HAlign::Left)
.with_name("connection_s");
let sidebar = LinearLayout::vertical().child(rooms).child(connections);
let layout = LinearLayout::horizontal().child(sidebar).child(chat);
siv.add_fullscreen_layer(layout);
Pronouns {
case_sensitive,
plural,
subject,
object,
possessive,
possessive_pronoun,
reflexive,
}
}
pub struct Identity {
pub name: String,
pub about: String,
pub pronouns: Option<Pronouns>,
pub fn edit_pronouns(siv: &mut Cursive) {
let labels = make_vertical_labels(&[
"Case-sensitive:",
"Plural:",
"Subject:",
"Object:",
"Possessive:",
"Possessive pronoun:",
"Reflexive:",
])
.fixed_width(20);
let mut values = LinearLayout::vertical();
let checkboxes = &["case_sensitive_edit", "plural_edit"];
for name in checkboxes.iter() {
values.add_child(
Checkbox::new()
.on_change(|siv, _value| {
let pronouns = get_edit_pronouns(siv);
update_pronouns_edit(siv, &pronouns);
})
.with_name(*name),
);
}
let edit_views = &[
"subject_edit",
"object_edit",
"possessive_edit",
"possessive_pronoun_edit",
"reflexive_edit",
];
for name in edit_views.iter() {
values.add_child(
EditView::new()
.on_edit(|siv, _text, _cursor| {
let pronouns = get_edit_pronouns(siv);
update_pronouns_edit(siv, &pronouns);
})
.with_name(*name),
);
}
let edit_layout = Panel::new(
LinearLayout::horizontal()
.child(labels)
.child(values.fixed_width(12)),
);
let example = make_example_usage_panel();
let layout = LinearLayout::horizontal().child(edit_layout).child(example);
let dialog = Dialog::around(layout)
.title("Edit Pronouns")
.button("Ok", |siv| {
let pronouns = get_edit_pronouns(siv);
update_pronouns_edit(siv, &pronouns);
siv.pop_layer();
})
.dismiss_button("Cancel");
siv.add_layer(dialog);
}
pub fn show_main(siv: &mut Cursive) {
let messages = LinearLayout::vertical()
.child(TextView::new("Hello, world!"))
.with_name("messages_list");
let messages = ScrollView::new(messages)
.scroll_strategy(ScrollStrategy::StickToBottom)
.with_name("messages_list_scroll")
.full_height();
let message_edit = EditView::new()
.on_submit(|siv, text| {
siv.call_on_name("message_edit", |message: &mut EditView| {
message.set_content("");
});
siv.with_user_data(|message_sender: &mut Sender<String>| {
message_sender.send(text.to_string()).unwrap();
});
add_message(
siv,
&crate::Message {
sender: "me".to_string(),
contents: text.to_owned(),
},
);
})
.with_name("message_edit")
.full_width();
let message_edit = Panel::new(message_edit)
.title("Send Message")
.title_position(HAlign::Left);
let upload_button = Button::new("Upload", |siv| {
siv.add_layer(Dialog::info("Uploading system goes here"))
});
let message_buttons = Panel::new(upload_button);
let message_bar = LinearLayout::horizontal()
.child(message_edit)
.child(message_buttons);
let chat = LinearLayout::vertical().child(messages).child(message_bar);
let chat = Panel::new(chat).title("Chat");
let mut rooms = SelectView::<&'static str>::new().on_submit(|siv, room: &str| {
let dialog = Dialog::info(format!("Selected room: {}", room));
siv.add_layer(dialog);
});
rooms.add_item("Room 1", "room_id_1");
rooms.add_item("Room 2", "room_id_2");
rooms.add_item("Room 3", "room_id_3");
let rooms = Dialog::around(rooms)
.button("Create...", |siv| {
siv.add_layer(Dialog::info("Room creation dialog goes here"))
})
.title("Rooms")
.title_position(HAlign::Left)
.with_name("room_select");
let mut connections = SelectView::new();
connections.add_item("Connection 1", "connection_1");
let connections = Dialog::around(connections)
.title("Connections")
.title_position(HAlign::Left)
.with_name("connection_s");
let sidebar = LinearLayout::vertical()
.child(rooms)
.child(connections)
.fixed_width(24);
let layout = LinearLayout::horizontal().child(sidebar).child(chat);
siv.add_fullscreen_layer(layout);
}
fn get_edit_contents(siv: &mut Cursive, name: &str) -> String {