multiverse/widgets/room_view/
input.rs

1use clap::{Parser, Subcommand};
2use crossterm::event::KeyEvent;
3use matrix_sdk::Room;
4use ratatui::{prelude::*, widgets::*};
5use style::palette::tailwind;
6use tui_textarea::TextArea;
7
8#[derive(Debug, Parser)]
9#[command(name = "multiverse", disable_help_flag = true, disable_help_subcommand = true)]
10struct Cli {
11    #[command(subcommand)]
12    command: Command,
13}
14
15#[derive(Debug, Subcommand)]
16pub enum Command {
17    Invite { user_id: String },
18    Leave,
19    Subscribe,
20    Unsubscribe,
21}
22
23pub enum MessageOrCommand {
24    Message(String),
25    Command(Command),
26}
27
28/// A widget representing a text input to send messages to a room.
29#[derive(Default)]
30pub struct Input {
31    /// The text area that will keep track of what the user has input.
32    textarea: TextArea<'static>,
33}
34
35impl Input {
36    /// Create a new empty [`Input`] widget.
37    pub fn new() -> Self {
38        let textarea = TextArea::default();
39
40        Self { textarea }
41    }
42
43    /// Receive a key press event and handle it.
44    pub fn handle_key_press(&mut self, event: KeyEvent) {
45        self.textarea.input(event);
46    }
47
48    /// Get the currently input text.
49    pub fn get_input(&self) -> Result<MessageOrCommand, clap::Error> {
50        let input = self.textarea.lines().join("\n");
51
52        if let Some(input) = input.strip_prefix("/") {
53            let arguments = input.split_whitespace();
54
55            // Clap expects the first argument to be the binary name, like when a command is
56            // invoked on the command line. Since we aren't a command line, but
57            // still find clap a neat command parser, let's give it what it
58            // expects so we can parse our commands.
59            Cli::try_parse_from(std::iter::once("multiverse").chain(arguments))
60                .map(|cli| MessageOrCommand::Command(cli.command))
61        } else {
62            Ok(MessageOrCommand::Message(input))
63        }
64    }
65
66    /// Is the input area empty, returns false if the user hasn't input anything
67    /// yet.
68    pub fn is_empty(&self) -> bool {
69        self.textarea.is_empty()
70    }
71
72    /// Clear the text from the input area.
73    pub fn clear(&mut self) {
74        self.textarea = TextArea::default();
75    }
76}
77
78impl<'a> StatefulWidget for &'a mut Input {
79    type State = Option<&'a Room>;
80
81    fn render(self, area: Rect, buf: &mut Buffer, room: &mut Self::State)
82    where
83        Self: Sized,
84    {
85        // Set the placeholder text depending on the encryption state of the room.
86        //
87        // We assume that the encryption state is synced because the RoomListService
88        // sets it as required state.
89        if let Some(room) = room.as_deref() {
90            let is_encrypted = room.encryption_state().is_encrypted();
91
92            if is_encrypted {
93                self.textarea.set_placeholder_text("(Send an encrypted message)");
94            } else {
95                self.textarea.set_placeholder_text("(Send an unencrypted message)");
96            }
97        } else {
98            self.textarea.set_placeholder_text("(No room selected)");
99        }
100
101        // Let's first create a block to set the background color.
102        let input_block = Block::new().borders(Borders::NONE).bg(tailwind::BLUE.c400);
103
104        // Now we set the block and we render the textarea.
105        self.textarea.set_block(input_block);
106        self.textarea.render(area, buf);
107    }
108}