Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Implement form input w/ tui-realm-textarea
Erik Kundt committed 2 years ago
commit a827e22796e0d57bd050d0ccffdc2adc70a0ff40
parent 1d26f1612cbc2ea6226a3ecdf716a95113750d49
6 files changed +282 -49
modified Cargo.lock
@@ -301,6 +301,31 @@ dependencies = [
]

[[package]]
+
name = "crossterm"
+
version = "0.25.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67"
+
dependencies = [
+
 "bitflags 1.3.2",
+
 "crossterm_winapi",
+
 "libc",
+
 "mio",
+
 "parking_lot",
+
 "signal-hook",
+
 "signal-hook-mio",
+
 "winapi",
+
]
+

+
[[package]]
+
name = "crossterm_winapi"
+
version = "0.9.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
+
dependencies = [
+
 "winapi",
+
]
+

+
[[package]]
name = "crypto-bigint"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -919,6 +944,16 @@ dependencies = [
]

[[package]]
+
name = "lock_api"
+
version = "0.4.10"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
+
dependencies = [
+
 "autocfg",
+
 "scopeguard",
+
]
+

+
[[package]]
name = "log"
version = "0.4.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -940,6 +975,18 @@ dependencies = [
]

[[package]]
+
name = "mio"
+
version = "0.8.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
+
dependencies = [
+
 "libc",
+
 "log",
+
 "wasi 0.11.0+wasi-snapshot-preview1",
+
 "windows-sys",
+
]
+

+
[[package]]
name = "multibase"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1063,6 +1110,29 @@ dependencies = [
]

[[package]]
+
name = "parking_lot"
+
version = "0.12.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
+
dependencies = [
+
 "lock_api",
+
 "parking_lot_core",
+
]
+

+
[[package]]
+
name = "parking_lot_core"
+
version = "0.9.8"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447"
+
dependencies = [
+
 "cfg-if",
+
 "libc",
+
 "redox_syscall 0.3.5",
+
 "smallvec",
+
 "windows-targets",
+
]
+

+
[[package]]
name = "pbkdf2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1336,6 +1406,7 @@ dependencies = [
 "textwrap 0.16.0",
 "timeago",
 "tui-realm-stdlib",
+
 "tui-realm-textarea",
 "tuirealm",
]

@@ -1523,6 +1594,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"

[[package]]
+
name = "scopeguard"
+
version = "1.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+

+
[[package]]
name = "sec1"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1603,6 +1680,36 @@ dependencies = [
]

[[package]]
+
name = "signal-hook"
+
version = "0.3.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8621587d4798caf8eb44879d42e56b9a93ea5dcd315a6487c357130095b62801"
+
dependencies = [
+
 "libc",
+
 "signal-hook-registry",
+
]
+

+
[[package]]
+
name = "signal-hook-mio"
+
version = "0.2.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af"
+
dependencies = [
+
 "libc",
+
 "mio",
+
 "signal-hook",
+
]
+

+
[[package]]
+
name = "signal-hook-registry"
+
version = "1.4.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1"
+
dependencies = [
+
 "libc",
+
]
+

+
[[package]]
name = "signature"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1902,6 +2009,7 @@ checksum = "ccdd26cbd674007e649a272da4475fb666d3aa0ad0531da7136db6fab0e5bad1"
dependencies = [
 "bitflags 1.3.2",
 "cassowary",
+
 "crossterm",
 "termion 1.5.6",
 "unicode-segmentation",
 "unicode-width",
@@ -1919,6 +2027,26 @@ dependencies = [
]

[[package]]
+
name = "tui-realm-textarea"
+
version = "1.1.1"
+
source = "git+https://github.com/erak/tui-realm-textarea.git?branch=no-borders#07f8344e0d7c6448abb9df96e900bac65b8856ad"
+
dependencies = [
+
 "lazy-regex",
+
 "tui-textarea",
+
 "tuirealm",
+
]
+

+
[[package]]
+
name = "tui-textarea"
+
version = "0.1.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4e7a086dc7ddcec25bfb9c47d5f3d85e3313c0c67eebe645d9e6b883452e56cc"
+
dependencies = [
+
 "crossterm",
+
 "tui",
+
]
+

+
[[package]]
name = "tuirealm"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
modified Cargo.toml
@@ -22,3 +22,4 @@ timeago = { version = "0.4.1" }
textwrap = { version = "0.16.0" }
tuirealm = { version = "1.8.0", default-features = false, features = [ "with-termion" ] }
tui-realm-stdlib = { version = "1.2.0", default-features = false, features = [ "with-termion" ] }
+
tui-realm-textarea = { git = "https://github.com/erak/tui-realm-textarea.git", branch = "no-borders", default-features = false, features = [ "with-termion" ] }
modified src/app/event.rs
@@ -6,6 +6,7 @@ use radicle_tui::ui::widget::common::container::{
    AppHeader, GlobalListener, LabeledContainer, Popup,
};
use radicle_tui::ui::widget::common::context::{ContextBar, Shortcuts};
+
use radicle_tui::ui::widget::common::form::TextInput;
use radicle_tui::ui::widget::common::list::PropertyList;
use radicle_tui::ui::widget::home::{Dashboard, IssueBrowser, PatchBrowser};
use radicle_tui::ui::widget::{issue, patch};
@@ -148,12 +149,18 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<issue::NewForm> {
                Some(Message::Tick)
            }
            Event::Keyboard(KeyEvent {
-
                code: Key::Char(ch),
-
                modifiers: KeyModifiers::NONE,
+
                code: Key::Enter, ..
            }) => {
-
                self.perform(Cmd::Type(ch));
+
                self.perform(Cmd::Custom(TextInput::CMD_NEWLINE));
                Some(Message::Tick)
            }
+
            Event::Keyboard(KeyEvent {
+
                code: Key::Char('s'),
+
                modifiers: KeyModifiers::ALT,
+
            }) => {
+
                self.perform(Cmd::Submit);
+
                None
+
            }
            Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
                Some(Message::Issue(IssueMessage::ClosePopup(IssueCid::NewForm)))
            }
@@ -168,11 +175,18 @@ impl tuirealm::Component<Message, NoUserEvent> for Widget<issue::NewForm> {
                Some(Message::Tick)
            }
            Event::Keyboard(KeyEvent {
-
                code: Key::Char('s'),
-
                modifiers: KeyModifiers::ALT,
+
                code: Key::Char(ch),
+
                modifiers: KeyModifiers::SHIFT,
            }) => {
-
                self.perform(Cmd::Submit);
-
                None
+
                self.perform(Cmd::Type(ch.to_ascii_uppercase()));
+
                Some(Message::Tick)
+
            }
+
            Event::Keyboard(KeyEvent {
+
                code: Key::Char(ch),
+
                ..
+
            }) => {
+
                self.perform(Cmd::Type(ch));
+
                Some(Message::Tick)
            }
            _ => None,
        }
modified src/app/page.rs
@@ -439,7 +439,9 @@ impl ViewPage for IssuePage {
        app.umount(&Cid::Issue(IssueCid::List))?;
        app.umount(&Cid::Issue(IssueCid::Details))?;
        app.umount(&Cid::Issue(IssueCid::Context))?;
-
        app.umount(&Cid::Issue(IssueCid::NewForm))?;
+
        if app.mounted(&Cid::Issue(IssueCid::NewForm)) {
+
            app.umount(&Cid::Issue(IssueCid::NewForm))?;
+
        }
        app.umount(&Cid::Issue(IssueCid::Shortcuts))?;
        Ok(())
    }
modified src/ui/widget/common/form.rs
@@ -1,21 +1,94 @@
-
use tui_realm_stdlib::Input;
+
use tui_realm_textarea::TextArea;
+

use tuirealm::command::{Cmd, CmdResult};
-
use tuirealm::tui::layout::{Constraint, Direction, Rect};
-
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State};
+
use tuirealm::props::Style;
+
use tuirealm::tui::layout::{Constraint, Direction, Margin, Rect};
+
use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State, StateValue};

use crate::ui::state::FormState;
use crate::ui::theme::Theme;
-
use crate::ui::widget::WidgetComponent;
+
use crate::ui::widget::{Widget, WidgetComponent};
+

+
use super::container::Container;
+
use super::label::Label;
+

+
pub struct TextInput {
+
    input: Widget<Container>,
+
    placeholder: Widget<Label>,
+
    show_placeholder: bool,
+
}
+

+
impl TextInput {
+
    pub const PROP_MULTILINE: &str = "multiline";
+
    pub const CMD_NEWLINE: &str = tui_realm_textarea::TEXTAREA_CMD_NEWLINE;
+

+
    pub fn new(theme: Theme, title: &str) -> Self {
+
        let input = TextArea::default()
+
            .cursor_line_style(Style::reset())
+
            .style(Style::default().fg(theme.colors.default_fg));
+
        let container = super::container(&theme, Box::new(input));
+

+
        Self {
+
            input: container,
+
            placeholder: super::label(title).foreground(theme.colors.input_placeholder_fg),
+
            show_placeholder: true,
+
        }
+
    }
+
}
+

+
impl WidgetComponent for TextInput {
+
    fn view(&mut self, properties: &Props, frame: &mut Frame, area: Rect) {
+
        let focus = properties
+
            .get_or(Attribute::Focus, AttrValue::Flag(false))
+
            .unwrap_flag();
+

+
        self.input.attr(Attribute::Focus, AttrValue::Flag(focus));
+
        self.input.view(frame, area);
+

+
        if self.show_placeholder {
+
            let inner = area.inner(&Margin {
+
                vertical: 1,
+
                horizontal: 2,
+
            });
+
            self.placeholder.view(frame, inner);
+
        }
+
    }
+

+
    fn state(&self) -> State {
+
        self.input.state()
+
    }
+

+
    fn perform(&mut self, properties: &Props, cmd: Cmd) -> CmdResult {
+
        let multiline = properties
+
            .get_or(
+
                Attribute::Custom(Self::PROP_MULTILINE),
+
                AttrValue::Flag(false),
+
            )
+
            .unwrap_flag();
+

+
        if !multiline && cmd == Cmd::Custom(Self::CMD_NEWLINE) {
+
            CmdResult::None
+
        } else {
+
            let result = self.input.perform(cmd);
+
            if let State::Vec(values) = self.input.state() {
+
                if let Some(StateValue::String(input)) = values.first() {
+
                    self.show_placeholder = input.is_empty();
+
                }
+
            }
+
            result
+
        }
+
    }
+
}

pub struct Form {
    // This form's fields: title, tags, assignees, description.
-
    inputs: Vec<Input>,
+
    inputs: Vec<Widget<TextInput>>,
    /// State that holds the current focus etc.
    state: FormState,
}

impl Form {
-
    pub fn new(_theme: Theme, inputs: Vec<Input>) -> Self {
+
    pub fn new(_theme: Theme, inputs: Vec<Widget<TextInput>>) -> Self {
        let state = FormState::new(Some(0), inputs.len());

        Self { inputs, state }
@@ -70,13 +143,46 @@ impl WidgetComponent for Form {
                self.state.focus_next();
                CmdResult::None
            }
+
            Cmd::Submit => {
+
                // Fold each input's vector of lines into a single string
+
                // that containes newlines and return a state vector with
+
                // each entry being the folded input string.
+
                let states = self
+
                    .inputs
+
                    .iter()
+
                    .map(|input| {
+
                        if let State::Vec(values) = input.state() {
+
                            let mut text = String::new();
+
                            let lines = values
+
                                .iter()
+
                                .map(|value| match value {
+
                                    StateValue::String(line) => line.clone(),
+
                                    _ => String::new(),
+
                                })
+
                                .collect::<Vec<_>>();
+

+
                            let mut lines = lines.iter().peekable();
+
                            while let Some(line) = lines.next() {
+
                                text.push_str(line);
+
                                if lines.peek().is_some() {
+
                                    text.push('\n');
+
                                }
+
                            }
+

+
                            StateValue::String(text)
+
                        } else {
+
                            StateValue::None
+
                        }
+
                    })
+
                    .collect::<Vec<_>>();
+
                CmdResult::Submit(State::Vec(states))
+
            }
            _ => {
                let focus = self.state.focus().unwrap_or(0);
                if let Some(input) = self.inputs.get_mut(focus) {
-
                    input.perform(cmd)
-
                } else {
-
                    CmdResult::None
+
                    return input.perform(cmd);
                }
+
                CmdResult::None
            }
        }
    }
modified src/ui/widget/issue.rs
@@ -3,10 +3,6 @@ use radicle::cob::thread::CommentId;

use radicle::cob::issue::Issue;
use radicle::cob::issue::IssueId;
-
use tui_realm_stdlib::Input;
-
use tuirealm::props::BorderType;
-
use tuirealm::props::Borders;
-
use tuirealm::props::Style;
use tuirealm::tui::layout::Constraint;
use tuirealm::tui::layout::Direction;
use tuirealm::tui::layout::Layout;
@@ -23,6 +19,7 @@ use crate::ui::cob;
use crate::ui::cob::IssueItem;
use crate::ui::context::Context;
use crate::ui::theme::Theme;
+
use crate::ui::widget::common::form::TextInput;

use super::*;

@@ -258,38 +255,17 @@ impl NewForm {
    pub fn new(theme: &Theme) -> Self {
        use tuirealm::props::Layout;

-
        let foreground = theme.colors.default_fg;
-
        let placeholder_style = Style::default().fg(theme.colors.input_placeholder_fg);
-
        let inactive_style = Style::default().fg(theme.colors.container_border_fg);
-
        let borders = Borders::default()
-
            .modifiers(BorderType::Rounded)
-
            .color(theme.colors.container_border_focus_fg);
-

-
        let title = Input::default()
-
            .foreground(foreground)
-
            .borders(borders.clone())
-
            .inactive(inactive_style)
-
            .placeholder("Title", placeholder_style);
-
        let tags = Input::default()
-
            .foreground(foreground)
-
            .borders(borders.clone())
-
            .inactive(inactive_style)
-
            .placeholder("Tags", placeholder_style);
-
        let assignees = Input::default()
-
            .foreground(foreground)
-
            .borders(borders.clone())
-
            .inactive(inactive_style)
-
            .placeholder("Assignees", placeholder_style);
-
        let description = Input::default()
-
            .foreground(foreground)
-
            .borders(borders)
-
            .inactive(inactive_style)
-
            .placeholder("Description", placeholder_style);
+
        let title = Widget::new(TextInput::new(theme.clone(), "Title"));
+
        let tags = Widget::new(TextInput::new(theme.clone(), "Tags"));
+
        let assignees = Widget::new(TextInput::new(theme.clone(), "Assignees"));
+
        let description = Widget::new(TextInput::new(theme.clone(), "Description"))
+
            .custom(TextInput::PROP_MULTILINE, AttrValue::Flag(true));

        let mut form = Widget::new(Form::new(
            theme.clone(),
            vec![title, tags, assignees, description],
        ));
+

        form.attr(
            Attribute::Layout,
            AttrValue::Layout(
@@ -322,7 +298,13 @@ impl WidgetComponent for NewForm {
    }

    fn perform(&mut self, _properties: &Props, cmd: Cmd) -> CmdResult {
-
        self.form.perform(cmd)
+
        match self.form.perform(cmd) {
+
            CmdResult::Submit(State::Vec(values)) => {
+
                println!("{:?}", values);
+
                CmdResult::None
+
            }
+
            _ => CmdResult::None,
+
        }
    }
}