Implement live caesar cipher

This commit is contained in:
Joscha 2024-01-05 23:21:06 +01:00
parent ab81c89854
commit ee7121b04e
8 changed files with 81 additions and 5 deletions

View file

@ -18,6 +18,7 @@ Procedure when bumping the version number:
### Added
- Support for setting window title
- More information to room list heading
- Key bindings for live caesar cipher de- and encoding
### Removed
- Key binding to open present page

View file

@ -104,6 +104,8 @@ default_bindings! {
pub fn mark_older_seen => ["ctrl+s"];
pub fn info => ["i"];
pub fn links => ["I"];
pub fn increase_caesar => ["c"];
pub fn decrease_caesar => ["C"];
}
}
@ -354,6 +356,12 @@ pub struct TreeAction {
/// List links found in message.
#[serde(default = "default::tree_action::links")]
pub links: KeyBinding,
/// Increase caesar cipher rotation.
#[serde(default = "default::tree_action::increase_caesar")]
pub increase_caesar: KeyBinding,
/// Decrease caesar cipher rotation.
#[serde(default = "default::tree_action::decrease_caesar")]
pub decrease_caesar: KeyBinding,
}
#[derive(Debug, Default, Deserialize, Document)]

View file

@ -11,6 +11,7 @@ use toss::widgets::{BoxedAsync, EditorState};
use toss::{Styled, WidgetExt};
use crate::store::{Msg, MsgStore};
use crate::util;
use self::cursor::Cursor;
use self::tree::TreeViewState;
@ -33,6 +34,7 @@ pub struct ChatState<M: Msg, S: MsgStore<M>> {
cursor: Cursor<M::Id>,
editor: EditorState,
caesar: i8,
mode: Mode,
tree: TreeViewState<M, S>,
@ -43,6 +45,7 @@ impl<M: Msg, S: MsgStore<M> + Clone> ChatState<M, S> {
Self {
cursor: Cursor::Bottom,
editor: EditorState::new(),
caesar: 0,
mode: Mode::Tree,
tree: TreeViewState::new(store.clone()),
@ -68,7 +71,13 @@ impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
match self.mode {
Mode::Tree => self
.tree
.widget(&mut self.cursor, &mut self.editor, nick, focused)
.widget(
&mut self.cursor,
&mut self.editor,
nick,
focused,
self.caesar,
)
.boxed_async(),
}
}
@ -85,7 +94,7 @@ impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
S: Send + Sync,
S::Error: Send,
{
match self.mode {
let reaction = match self.mode {
Mode::Tree => {
self.tree
.handle_input_event(
@ -95,9 +104,28 @@ impl<M: Msg, S: MsgStore<M>> ChatState<M, S> {
&mut self.editor,
can_compose,
)
.await
.await?
}
}
};
Ok(match reaction {
Reaction::Composed { parent, content } if self.caesar != 0 => {
let content = util::caesar(&content, self.caesar);
Reaction::Composed { parent, content }
}
Reaction::NotHandled if event.matches(&keys.tree.action.increase_caesar) => {
self.caesar = (self.caesar + 1).rem_euclid(26);
Reaction::Handled
}
Reaction::NotHandled if event.matches(&keys.tree.action.decrease_caesar) => {
self.caesar = (self.caesar - 1).rem_euclid(26);
Reaction::Handled
}
reaction => reaction,
})
}
pub fn cursor(&self) -> Option<&M::Id> {

View file

@ -386,6 +386,7 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
editor: &'a mut EditorState,
nick: String,
focused: bool,
caesar: i8,
) -> TreeView<'a, M, S> {
TreeView {
state: self,
@ -393,6 +394,7 @@ impl<M: Msg, S: MsgStore<M>> TreeViewState<M, S> {
editor,
nick,
focused,
caesar,
}
}
}
@ -405,6 +407,7 @@ pub struct TreeView<'a, M: Msg, S: MsgStore<M>> {
nick: String,
focused: bool,
caesar: i8,
}
#[async_trait]
@ -432,6 +435,7 @@ where
size,
nick: self.nick.clone(),
focused: self.focused,
caesar: self.caesar,
last_cursor: self.state.last_cursor.clone(),
last_cursor_top: self.state.last_cursor_top,
};

View file

@ -72,6 +72,7 @@ pub struct TreeContext<Id> {
pub size: Size,
pub nick: String,
pub focused: bool,
pub caesar: i8,
pub last_cursor: Cursor<Id>,
pub last_cursor_top: i32,
}
@ -190,7 +191,7 @@ where
};
let highlighted = highlighted && self.context.focused;
let widget = widgets::msg(highlighted, indent, msg, folded_info);
let widget = widgets::msg(highlighted, indent, msg, self.context.caesar, folded_info);
let widget = Self::predraw(widget, self.context.size, self.widthdb);
Block::new(TreeBlockId::Msg(msg_id), widget, true)
}

View file

@ -20,6 +20,7 @@ where
size: self.last_size,
nick: self.last_nick.clone(),
focused: true,
caesar: 0,
last_cursor: self.last_cursor.clone(),
last_cursor_top: self.last_cursor_top,
}

View file

@ -7,6 +7,7 @@ use toss::{Style, Styled, WidgetExt};
use crate::store::Msg;
use crate::ui::chat::widgets::{Indent, Seen, Time};
use crate::ui::ChatMsg;
use crate::util;
pub const PLACEHOLDER: &str = "[...]";
@ -30,6 +31,10 @@ fn style_indent(highlighted: bool) -> Style {
}
}
fn style_caesar() -> Style {
Style::new().green()
}
fn style_info() -> Style {
Style::new().italic().dark_grey()
}
@ -46,10 +51,19 @@ pub fn msg<M: Msg + ChatMsg>(
highlighted: bool,
indent: usize,
msg: &M,
caesar: i8,
folded_info: Option<usize>,
) -> Boxed<'static, Infallible> {
let (nick, mut content) = msg.styled();
if caesar != 0 {
// Apply caesar in inverse because we're decoding
let rotated = util::caesar(content.text(), -caesar);
content = content
.then_plain("\n")
.then(format!("{rotated} [rot{caesar}]"), style_caesar());
}
if let Some(amount) = folded_info {
content = content
.then_plain("\n")

View file

@ -48,3 +48,22 @@ pub fn convert_to_time_zone(tz: &TimeZone, time: OffsetDateTime) -> Option<Offse
Some(time.to_offset(utc_offset))
}
pub fn caesar(text: &str, by: i8) -> String {
let by = by.rem_euclid(26) as u8;
text.chars()
.map(|c| {
if c.is_ascii_lowercase() {
let c = c as u8 - b'a';
let c = (c + by) % 26;
(c + b'a') as char
} else if c.is_ascii_uppercase() {
let c = c as u8 - b'A';
let c = (c + by) % 26;
(c + b'A') as char
} else {
c
}
})
.collect()
}