Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
Use `tui-textarea` in textarea widget
Merged did:key:z6MkswQE...2C1V opened 1 year ago
10 files changed +395 -273 6484ede2 35830ecb
modified Cargo.lock
@@ -479,6 +479,31 @@ dependencies = [
]

[[package]]
+
name = "crossterm"
+
version = "0.27.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
+
dependencies = [
+
 "bitflags 2.4.1",
+
 "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.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1168,6 +1193,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09"
dependencies = [
 "libc",
+
 "log",
 "wasi 0.11.0+wasi-snapshot-preview1",
 "windows-sys 0.48.0",
]
@@ -1838,6 +1864,7 @@ dependencies = [
 "timeago",
 "tokio",
 "tokio-stream",
+
 "tui-textarea",
]

[[package]]
@@ -1878,6 +1905,7 @@ dependencies = [
 "bitflags 2.4.1",
 "cassowary",
 "compact_str",
+
 "crossterm",
 "indoc",
 "itertools",
 "lru",
@@ -2110,6 +2138,17 @@ dependencies = [
]

[[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"
@@ -2718,6 +2757,17 @@ dependencies = [
]

[[package]]
+
name = "tui-textarea"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a3e38ced1f941a9cfc923fbf2fe6858443c42cc5220bfd35bdd3648371e7bd8e"
+
dependencies = [
+
 "crossterm",
+
 "ratatui",
+
 "unicode-width",
+
]
+

+
[[package]]
name = "typenum"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2767,9 +2817,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"

[[package]]
name = "unicode-width"
-
version = "0.1.10"
+
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"

[[package]]
name = "universal-hash"
modified Cargo.toml
@@ -44,3 +44,4 @@ textwrap = { version = "0.16.0" }
thiserror = { version = "1" }
tokio = { version = "1.32.0", features = ["full"] }
tokio-stream = { version = "0.1.14" }
+
tui-textarea = "0.4.0"

\ No newline at end of file
modified bin/commands/inbox/select.rs
@@ -26,7 +26,7 @@ use tui::store;
use tui::store::StateValue;
use tui::ui::span;
use tui::ui::widget::container::{Column, Container, Footer, FooterProps, Header, HeaderProps};
-
use tui::ui::widget::text::{TextArea, TextAreaProps};
+
use tui::ui::widget::input::{TextArea, TextAreaProps};
use tui::ui::widget::window::{Page, PageProps, Shortcuts, ShortcutsProps, Window, WindowProps};
use tui::ui::widget::{ToWidget, Widget};
use tui::{BoxedAny, Channel, Exit, PageStack};
@@ -83,6 +83,7 @@ impl BrowserState {
#[derive(Clone, Debug)]
pub struct HelpState {
    scroll: usize,
+
    cursor: (usize, usize),
}

#[derive(Clone, Debug)]
@@ -196,7 +197,10 @@ impl TryFrom<&Context> for State {
                search,
                show_search: false,
            },
-
            help: HelpState { scroll: 0 },
+
            help: HelpState {
+
                scroll: 0,
+
                cursor: (0, 0),
+
            },
        })
    }
}
@@ -219,6 +223,7 @@ pub enum Message {
    LeavePage,
    ScrollHelp {
        scroll: usize,
+
        cursor: (usize, usize),
    },
}

@@ -273,8 +278,9 @@ impl store::State<Selection> for State {
                self.pages.pop();
                None
            }
-
            Message::ScrollHelp { scroll } => {
+
            Message::ScrollHelp { scroll, cursor } => {
                self.help.scroll = scroll;
+
                self.help.cursor = cursor;
                None
            }
        }
@@ -378,13 +384,13 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
            TextArea::default()
                .to_widget(tx.clone())
                .on_event(|_, s, _| {
-
                    Some(Message::ScrollHelp {
-
                        scroll: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
-
                    })
+
                    let (scroll, cursor) = s.and_then(|p| p.unwrap_textarea()).unwrap_or_default();
+
                    Some(Message::ScrollHelp { scroll, cursor })
                })
-
                .on_update(|_| {
+
                .on_update(|state: &State| {
                    TextAreaProps::default()
-
                        .text(&help_text())
+
                        .content(help_text())
+
                        .cursor(state.help.cursor)
                        .to_boxed_any()
                        .into()
                }),
@@ -436,32 +442,32 @@ fn help_text() -> Text<'static> {
            Line::raw(""),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "↑,k")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("move cursor one line up").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "↓,j")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("move cursor one line down").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "PageUp")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("move cursor one page up").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "PageDown")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("move cursor one page down").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "Home")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("move cursor to the first line").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "End")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("move cursor to the last line").gray().dim(),
            ]),
            Line::raw(""),
@@ -469,32 +475,32 @@ fn help_text() -> Text<'static> {
            Line::raw(""),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "enter")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("Select notification (if --mode id)").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "enter")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("Show notification").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "c")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("Clear notifications").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "/")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("Search").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "?")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("Show help").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "Esc")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("Quit / cancel").gray().dim(),
            ]),
            Line::raw(""),
@@ -502,18 +508,16 @@ fn help_text() -> Text<'static> {
            Line::raw(""),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "Pattern")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("is:<state> | is:patch | is:issue | <search>")
                    .gray()
                    .dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "Example")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("is:unseen is:patch Print").gray().dim(),
            ]),
-
            Line::raw(""),
-
            Line::raw(""),
        ]
        .to_vec(),
    )
modified bin/commands/issue/select.rs
@@ -21,7 +21,7 @@ use tui::store;
use tui::store::StateValue;
use tui::ui::span;
use tui::ui::widget::container::{Column, Container, Footer, FooterProps, Header, HeaderProps};
-
use tui::ui::widget::text::{TextArea, TextAreaProps};
+
use tui::ui::widget::input::{TextArea, TextAreaProps};
use tui::ui::widget::window::{Page, PageProps, Shortcuts, ShortcutsProps, Window, WindowProps};
use tui::ui::widget::{ToWidget, Widget};

@@ -77,6 +77,7 @@ impl BrowserState {
#[derive(Clone, Debug)]
pub struct HelpState {
    scroll: usize,
+
    cursor: (usize, usize),
}

#[derive(Clone, Debug)]
@@ -115,7 +116,10 @@ impl TryFrom<&Context> for State {
                search,
                show_search: false,
            },
-
            help: HelpState { scroll: 0 },
+
            help: HelpState {
+
                scroll: 0,
+
                cursor: (0, 0),
+
            },
        })
    }
}
@@ -138,6 +142,7 @@ pub enum Message {
    LeavePage,
    ScrollHelp {
        scroll: usize,
+
        cursor: (usize, usize),
    },
}

@@ -192,8 +197,9 @@ impl store::State<Selection> for State {
                self.pages.pop();
                None
            }
-
            Message::ScrollHelp { scroll } => {
+
            Message::ScrollHelp { scroll, cursor } => {
                self.help.scroll = scroll;
+
                self.help.cursor = cursor;
                None
            }
        }
@@ -297,13 +303,13 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
            TextArea::default()
                .to_widget(tx.clone())
                .on_event(|_, s, _| {
-
                    Some(Message::ScrollHelp {
-
                        scroll: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
-
                    })
+
                    let (scroll, cursor) = s.and_then(|p| p.unwrap_textarea()).unwrap_or_default();
+
                    Some(Message::ScrollHelp { scroll, cursor })
                })
-
                .on_update(|_| {
+
                .on_update(|state: &State| {
                    TextAreaProps::default()
-
                        .text(&help_text())
+
                        .content(help_text())
+
                        .cursor(state.help.cursor)
                        .to_boxed_any()
                        .into()
                }),
@@ -356,7 +362,7 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "↑,k")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("move cursor one line up").gray().dim(),
                ]
                .to_vec(),
@@ -364,7 +370,7 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "↓,j")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("move cursor one line down").gray().dim(),
                ]
                .to_vec(),
@@ -372,7 +378,7 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "PageUp")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("move cursor one page up").gray().dim(),
                ]
                .to_vec(),
@@ -380,7 +386,7 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "PageDown")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("move cursor one page down").gray().dim(),
                ]
                .to_vec(),
@@ -388,7 +394,7 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "Home")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("move cursor to the first line").gray().dim(),
                ]
                .to_vec(),
@@ -396,7 +402,7 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "End")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("move cursor to the last line").gray().dim(),
                ]
                .to_vec(),
@@ -407,7 +413,7 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "enter")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("Select issue (if --mode id)").gray().dim(),
                ]
                .to_vec(),
@@ -415,7 +421,7 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "enter")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("Show issue").gray().dim(),
                ]
                .to_vec(),
@@ -423,7 +429,7 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "e")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("Edit patch").gray().dim(),
                ]
                .to_vec(),
@@ -431,7 +437,7 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "/")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("Search").gray().dim(),
                ]
                .to_vec(),
@@ -439,7 +445,7 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "?")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("Show help").gray().dim(),
                ]
                .to_vec(),
@@ -447,7 +453,7 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "Esc")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("Quit / cancel").gray().dim(),
                ]
                .to_vec(),
@@ -458,7 +464,7 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "Pattern")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("is:<state> | is:authored | is:assigned | authors:[<did>, ...] | assignees:[<did>, ...] | <search>")
                        .gray()
                        .dim(),
@@ -468,13 +474,11 @@ fn help_text() -> Text<'static> {
            Line::from(
                [
                    Span::raw(format!("{key:>10}", key = "Example")).gray(),
-
                    Span::raw(" "),
+
                    Span::raw(": "),
                    Span::raw("is:solved is:authored alias").gray().dim(),
                ]
                .to_vec(),
            ),
-
            Line::raw(""),
-
            Line::raw(""),
        ]
        .to_vec())
}
modified bin/commands/patch/select.rs
@@ -19,7 +19,7 @@ use termion::event::Key;
use tui::store;
use tui::ui::span;
use tui::ui::widget::container::{Column, Container, Footer, FooterProps, Header, HeaderProps};
-
use tui::ui::widget::text::{TextArea, TextAreaProps};
+
use tui::ui::widget::input::{TextArea, TextAreaProps};
use tui::ui::widget::window::{Page, PageProps, Shortcuts, ShortcutsProps, Window, WindowProps};
use tui::ui::widget::{ToWidget, Widget};

@@ -74,6 +74,7 @@ impl BrowserState {
#[derive(Clone, Debug)]
pub struct HelpState {
    scroll: usize,
+
    cursor: (usize, usize),
}

#[derive(Clone, Debug)]
@@ -112,7 +113,10 @@ impl TryFrom<&Context> for State {
                search,
                show_search: false,
            },
-
            help: HelpState { scroll: 0 },
+
            help: HelpState {
+
                scroll: 0,
+
                cursor: (0, 0),
+
            },
        })
    }
}
@@ -135,6 +139,7 @@ pub enum Message {
    LeavePage,
    ScrollHelp {
        scroll: usize,
+
        cursor: (usize, usize),
    },
}

@@ -189,8 +194,9 @@ impl store::State<Selection> for State {
                self.pages.pop();
                None
            }
-
            Message::ScrollHelp { scroll } => {
+
            Message::ScrollHelp { scroll, cursor } => {
                self.help.scroll = scroll;
+
                self.help.cursor = cursor;
                None
            }
        }
@@ -295,13 +301,13 @@ fn help_page(_state: &State, channel: &Channel<Message>) -> Widget<State, Messag
            TextArea::default()
                .to_widget(tx.clone())
                .on_event(|_, s, _| {
-
                    Some(Message::ScrollHelp {
-
                        scroll: s.and_then(|p| p.unwrap_usize()).unwrap_or_default(),
-
                    })
+
                    let (scroll, cursor) = s.and_then(|p| p.unwrap_textarea()).unwrap_or_default();
+
                    Some(Message::ScrollHelp { scroll, cursor })
                })
-
                .on_update(|_| {
+
                .on_update(|state: &State| {
                    TextAreaProps::default()
-
                        .text(&help_text())
+
                        .content(help_text())
+
                        .cursor(state.help.cursor)
                        .to_boxed_any()
                        .into()
                }),
@@ -353,32 +359,32 @@ fn help_text() -> Text<'static> {
            Line::raw(""),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "↑,k")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("move cursor one line up").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "↓,j")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("move cursor one line down").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "PageUp")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("move cursor one page up").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "PageDown")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("move cursor one page down").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "Home")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("move cursor to the first line").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "End")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("move cursor to the last line").gray().dim(),
            ]),
            Line::raw(""),
@@ -386,37 +392,37 @@ fn help_text() -> Text<'static> {
            Line::raw(""),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "enter")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("Select patch (if --mode id)").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "enter")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("Show patch").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "c")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("Checkout patch").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "d")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("Show patch diff").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "/")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("Search").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "?")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("Show help").gray().dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "Esc")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("Quit / cancel").gray().dim(),
            ]),
            Line::raw(""),
@@ -424,18 +430,16 @@ fn help_text() -> Text<'static> {
            Line::raw(""),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "Pattern")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("is:<state> | is:authored | authors:[<did>, <did>] | <search>")
                    .gray()
                    .dim(),
            ]),
            Line::from(vec![
                Span::raw(format!("{key:>10}", key = "Example")).gray(),
-
                Span::raw(" "),
+
                Span::raw(": "),
                Span::raw("is:open is:authored improve").gray().dim(),
            ]),
-
            Line::raw(""),
-
            Line::raw(""),
        ]
        .to_vec(),
    )
modified examples/basic.rs
@@ -8,7 +8,7 @@ use radicle_tui as tui;

use tui::store;
use tui::ui::widget::container::{Column, Container, Header, HeaderProps};
-
use tui::ui::widget::text::{TextArea, TextAreaProps};
+
use tui::ui::widget::input::{TextArea, TextAreaProps};
use tui::ui::widget::window::{Page, Shortcuts, ShortcutsProps, Window, WindowProps};
use tui::ui::widget::ToWidget;
use tui::{BoxedAny, Channel, Exit};
@@ -73,7 +73,7 @@ pub async fn main() -> Result<()> {
                .content(TextArea::default().to_widget(sender.clone()).on_update(
                    |state: &State| {
                        TextAreaProps::default()
-
                            .text(&state.content.clone().into())
+
                            .content(state.content.clone())
                            .can_scroll(false)
                            .to_boxed_any()
                            .into()
modified examples/hello.rs
@@ -8,7 +8,7 @@ use ratatui::text::Text;
use radicle_tui as tui;

use tui::store;
-
use tui::ui::widget::text::{TextArea, TextAreaProps};
+
use tui::ui::widget::input::{TextArea, TextAreaProps};
use tui::ui::widget::ToWidget;
use tui::{BoxedAny, Channel, Exit};

@@ -62,7 +62,8 @@ pub async fn main() -> Result<()> {
        })
        .on_update(|state: &State| {
            TextAreaProps::default()
-
                .text(&Text::styled(state.alien.clone(), Color::Rgb(85, 85, 255)))
+
                .content(Text::styled(state.alien.clone(), Color::Rgb(85, 85, 255)))
+
                .can_scroll(false)
                .to_boxed_any()
                .into()
        });
modified src/ui/widget.rs
@@ -1,7 +1,6 @@
pub mod container;
pub mod input;
pub mod list;
-
pub mod text;
pub mod utils;
pub mod window;

@@ -63,7 +62,14 @@ impl From<&'static dyn Any> for ViewProps {
pub enum ViewState {
    USize(usize),
    String(String),
-
    Table { selected: usize, scroll: usize },
+
    Table {
+
        selected: usize,
+
        scroll: usize,
+
    },
+
    TextArea {
+
        scroll: usize,
+
        cursor: (usize, usize),
+
    },
}

impl ViewState {
@@ -87,6 +93,13 @@ impl ViewState {
            _ => None,
        }
    }
+

+
    pub fn unwrap_textarea(&self) -> Option<(usize, (usize, usize))> {
+
        match self {
+
            ViewState::TextArea { scroll, cursor } => Some((*scroll, *cursor)),
+
            _ => None,
+
        }
+
    }
}

#[derive(Clone, Default)]
modified src/ui/widget/input.rs
@@ -3,11 +3,11 @@ use std::marker::PhantomData;
use ratatui::Frame;
use termion::event::Key;

-
use ratatui::layout::{Constraint, Layout};
-
use ratatui::style::Stylize;
-
use ratatui::text::{Line, Span};
+
use ratatui::layout::{Alignment, Constraint, Layout};
+
use ratatui::style::{Style, Stylize};
+
use ratatui::text::{Line, Span, Text};

-
use super::{RenderProps, View, ViewProps, ViewState};
+
use super::{utils, RenderProps, View, ViewProps, ViewState};

#[derive(Clone)]
pub struct TextFieldProps {
@@ -281,3 +281,236 @@ where
        }
    }
}
+

+
/// Configuration of a `TextArea`'s internal progress display.
+
#[derive(Default, Clone)]
+
pub struct TextAreaProgressInfo {
+
    scroll: bool,
+
    cursor: bool,
+
}
+

+
impl TextAreaProgressInfo {
+
    pub fn scroll(mut self, scroll: bool) -> Self {
+
        self.scroll = scroll;
+
        self
+
    }
+

+
    pub fn cursor(mut self, cursor: bool) -> Self {
+
        self.cursor = cursor;
+
        self
+
    }
+

+
    pub fn is_rendered(&self) -> bool {
+
        self.scroll || self.cursor
+
    }
+
}
+

+
#[derive(Clone)]
+
pub struct TextAreaProps<'a> {
+
    content: Text<'a>,
+
    cursor: (usize, usize),
+
    can_scroll: bool,
+
    progress_info: TextAreaProgressInfo,
+
}
+

+
impl<'a> Default for TextAreaProps<'a> {
+
    fn default() -> Self {
+
        Self {
+
            content: String::new().into(),
+
            cursor: (0, 0),
+
            can_scroll: true,
+
            progress_info: TextAreaProgressInfo::default(),
+
        }
+
    }
+
}
+

+
impl<'a> TextAreaProps<'a> {
+
    pub fn content<T>(mut self, content: T) -> Self
+
    where
+
        T: Into<Text<'a>>,
+
    {
+
        self.content = content.into();
+
        self
+
    }
+

+
    pub fn cursor(mut self, cursor: (usize, usize)) -> Self {
+
        self.cursor = cursor;
+
        self
+
    }
+

+
    pub fn progress_info(mut self, progress_info: TextAreaProgressInfo) -> Self {
+
        self.progress_info = progress_info;
+
        self
+
    }
+

+
    pub fn can_scroll(mut self, can_scroll: bool) -> Self {
+
        self.can_scroll = can_scroll;
+
        self
+
    }
+
}
+

+
pub struct TextArea<'a, S, M> {
+
    phantom: PhantomData<(S, M)>,
+
    textarea: tui_textarea::TextArea<'a>,
+
    height: u16,
+
}
+

+
impl<'a, S, M> Default for TextArea<'a, S, M> {
+
    fn default() -> Self {
+
        Self {
+
            phantom: PhantomData,
+
            textarea: tui_textarea::TextArea::default(),
+
            height: 0,
+
        }
+
    }
+
}
+

+
impl<'a, S, M> View for TextArea<'a, S, M> {
+
    type State = S;
+
    type Message = M;
+

+
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
+
        let default = TextAreaProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<TextAreaProps>())
+
            .unwrap_or(&default);
+

+
        if props.can_scroll {
+
            match key {
+
                Key::Left => {
+
                    self.textarea.input(tui_textarea::Input {
+
                        key: tui_textarea::Key::Left,
+
                        ..Default::default()
+
                    });
+
                }
+
                Key::Right => {
+
                    self.textarea.input(tui_textarea::Input {
+
                        key: tui_textarea::Key::Right,
+
                        ..Default::default()
+
                    });
+
                }
+
                Key::Up => {
+
                    self.textarea.input(tui_textarea::Input {
+
                        key: tui_textarea::Key::Up,
+
                        ..Default::default()
+
                    });
+
                }
+
                Key::Down => {
+
                    self.textarea.input(tui_textarea::Input {
+
                        key: tui_textarea::Key::Down,
+
                        ..Default::default()
+
                    });
+
                }
+
                _ => {}
+
            }
+
        }
+

+
        None
+
    }
+

+
    fn update(&mut self, props: Option<&ViewProps>, _state: &Self::State) {
+
        let default = TextAreaProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<TextAreaProps>())
+
            .unwrap_or(&default);
+

+
        self.textarea = tui_textarea::TextArea::new(
+
            props
+
                .content
+
                .lines
+
                .iter()
+
                .map(|line| line.to_string())
+
                .collect::<Vec<_>>(),
+
        );
+
    }
+

+
    fn render(&mut self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
+
        let default = TextAreaProps::default();
+
        let props = props
+
            .and_then(|props| props.inner_ref::<TextAreaProps>())
+
            .unwrap_or(&default);
+

+
        let [area] = Layout::default()
+
            .constraints([Constraint::Min(1)])
+
            .horizontal_margin(1)
+
            .areas(render.area);
+

+
        let cursor_line_style = Style::default();
+

+
        let cursor_style = if render.focus {
+
            Style::default().reversed()
+
        } else {
+
            cursor_line_style
+
        };
+

+
        let content_style = if render.focus {
+
            Style::default()
+
        } else {
+
            Style::default().dim()
+
        };
+

+
        self.height = render.area.height;
+

+
        self.textarea.move_cursor(tui_textarea::CursorMove::Jump(
+
            props.cursor.0 as u16,
+
            props.cursor.1 as u16,
+
        ));
+
        self.textarea.set_cursor_line_style(cursor_line_style);
+
        self.textarea.set_cursor_style(cursor_style);
+
        self.textarea.set_style(content_style);
+

+
        if props.progress_info.is_rendered() {
+
            let [content_area, progress_area] =
+
                Layout::vertical([Constraint::Min(1), Constraint::Length(1)]).areas(area);
+

+
            let mut progress_info = vec![];
+

+
            if props.progress_info.scroll {
+
                progress_info.push(Span::styled(
+
                    format!(
+
                        "{}%",
+
                        utils::scroll::percent_absolute(
+
                            self.textarea.cursor().0,
+
                            self.textarea.lines().len(),
+
                            content_area.height.into()
+
                        )
+
                    ),
+
                    Style::default().dim(),
+
                ))
+
            }
+

+
            if props.progress_info.scroll && props.progress_info.cursor {
+
                progress_info.push(Span::raw(" "));
+
            }
+

+
            if props.progress_info.cursor {
+
                progress_info.push(Span::styled(
+
                    format!(
+
                        "[{},{}]",
+
                        self.textarea.cursor().0,
+
                        self.textarea.cursor().1
+
                    ),
+
                    Style::default().dim(),
+
                ))
+
            }
+

+
            let line = Line::from(progress_info).alignment(Alignment::Right);
+

+
            frame.render_widget(self.textarea.widget(), content_area);
+
            frame.render_widget(line, progress_area);
+
        } else {
+
            frame.render_widget(self.textarea.widget(), area);
+
        }
+
    }
+

+
    fn view_state(&self) -> Option<ViewState> {
+
        Some(ViewState::TextArea {
+
            scroll: utils::scroll::percent_absolute(
+
                self.textarea.cursor().0.saturating_sub(self.height.into()),
+
                self.textarea.lines().len(),
+
                self.height.into(),
+
            ),
+
            cursor: self.textarea.cursor(),
+
        })
+
    }
+
}
deleted src/ui/widget/text.rs
@@ -1,188 +0,0 @@
-
use std::marker::PhantomData;
-

-
use termion::event::Key;
-

-
use ratatui::layout::{Constraint, Layout};
-
use ratatui::text::Text;
-
use ratatui::Frame;
-

-
use super::utils;
-
use super::{RenderProps, View, ViewProps, ViewState};
-

-
#[derive(Clone)]
-
pub struct TextAreaProps<'a> {
-
    pub content: Text<'a>,
-
    pub has_header: bool,
-
    pub has_footer: bool,
-
    pub progress: usize,
-
    pub can_scroll: bool,
-
}
-

-
impl<'a> TextAreaProps<'a> {
-
    pub fn text(mut self, text: &Text<'a>) -> Self {
-
        self.content = text.clone();
-
        self
-
    }
-

-
    pub fn can_scroll(mut self, can_scroll: bool) -> Self {
-
        self.can_scroll = can_scroll;
-
        self
-
    }
-
}
-

-
impl<'a> Default for TextAreaProps<'a> {
-
    fn default() -> Self {
-
        Self {
-
            content: Text::raw(""),
-
            has_header: false,
-
            has_footer: false,
-
            progress: 0,
-
            can_scroll: true,
-
        }
-
    }
-
}
-

-
#[derive(Clone)]
-
struct TextAreaState {
-
    /// Internal offset
-
    pub offset: usize,
-
    /// Internal progress
-
    pub progress: usize,
-
}
-

-
pub struct TextArea<S, M> {
-
    /// Internal state
-
    state: TextAreaState,
-
    /// Phantom
-
    phantom: PhantomData<(S, M)>,
-
    /// Current render height
-
    height: u16,
-
}
-

-
impl<S, M> Default for TextArea<S, M> {
-
    fn default() -> Self {
-
        Self {
-
            state: TextAreaState {
-
                offset: 0,
-
                progress: 0,
-
            },
-
            phantom: PhantomData,
-
            height: 1,
-
        }
-
    }
-
}
-

-
impl<S, M> TextArea<S, M> {
-
    fn scroll(&self) -> (u16, u16) {
-
        (self.state.offset as u16, 0)
-
    }
-

-
    fn prev(&mut self, len: usize, page_size: usize) -> (u16, u16) {
-
        self.state.offset = self.state.offset.saturating_sub(1);
-
        self.state.progress = utils::scroll::percent_absolute(self.state.offset, len, page_size);
-
        self.scroll()
-
    }
-

-
    fn next(&mut self, len: usize, page_size: usize) -> (u16, u16) {
-
        if self.state.progress < 100 {
-
            self.state.offset = self.state.offset.saturating_add(1);
-
            self.state.progress =
-
                utils::scroll::percent_absolute(self.state.offset, len, page_size);
-
        }
-

-
        self.scroll()
-
    }
-

-
    fn prev_page(&mut self, len: usize, page_size: usize) -> (u16, u16) {
-
        self.state.offset = self.state.offset.saturating_sub(page_size);
-
        self.state.progress = utils::scroll::percent_absolute(self.state.offset, len, page_size);
-
        self.scroll()
-
    }
-

-
    fn next_page(&mut self, len: usize, page_size: usize) -> (u16, u16) {
-
        let end = len.saturating_sub(page_size);
-

-
        self.state.offset = std::cmp::min(self.state.offset.saturating_add(page_size), end);
-
        self.state.progress = utils::scroll::percent_absolute(self.state.offset, len, page_size);
-
        self.scroll()
-
    }
-

-
    fn begin(&mut self, len: usize, page_size: usize) -> (u16, u16) {
-
        self.state.offset = 0;
-
        self.state.progress = utils::scroll::percent_absolute(self.state.offset, len, page_size);
-
        self.scroll()
-
    }
-

-
    fn end(&mut self, len: usize, page_size: usize) -> (u16, u16) {
-
        self.state.offset = len.saturating_sub(page_size);
-
        self.state.progress = utils::scroll::percent_absolute(self.state.offset, len, page_size);
-
        self.scroll()
-
    }
-
}
-

-
impl<S, M> View for TextArea<S, M>
-
where
-
    S: 'static,
-
    M: 'static,
-
{
-
    type Message = M;
-
    type State = S;
-

-
    fn handle_event(&mut self, props: Option<&ViewProps>, key: Key) -> Option<Self::Message> {
-
        let default = TextAreaProps::default();
-
        let props = props
-
            .and_then(|props| props.inner_ref::<TextAreaProps>())
-
            .unwrap_or(&default);
-

-
        let len = props.content.lines.len() + 1;
-
        let page_size = self.height as usize;
-

-
        if props.can_scroll {
-
            match key {
-
                Key::Up | Key::Char('k') => {
-
                    self.prev(len, page_size);
-
                }
-
                Key::Down | Key::Char('j') => {
-
                    self.next(len, page_size);
-
                }
-
                Key::PageUp => {
-
                    self.prev_page(len, page_size);
-
                }
-
                Key::PageDown => {
-
                    self.next_page(len, page_size);
-
                }
-
                Key::Home => {
-
                    self.begin(len, page_size);
-
                }
-
                Key::End => {
-
                    self.end(len, page_size);
-
                }
-
                _ => {}
-
            }
-
        }
-

-
        None
-
    }
-

-
    fn render(&mut self, props: Option<&ViewProps>, render: RenderProps, frame: &mut Frame) {
-
        self.height = render.area.height;
-

-
        let default = TextAreaProps::default();
-
        let props = props
-
            .and_then(|props| props.inner_ref::<TextAreaProps>())
-
            .unwrap_or(&default);
-

-
        let [content_area] = Layout::horizontal([Constraint::Min(1)])
-
            .horizontal_margin(1)
-
            .areas(render.area);
-
        let content = ratatui::widgets::Paragraph::new(props.content.clone())
-
            .style(props.content.style)
-
            .scroll((self.state.offset as u16, 0));
-

-
        frame.render_widget(content, content_area);
-
    }
-

-
    fn view_state(&self) -> Option<ViewState> {
-
        Some(ViewState::USize(self.state.progress))
-
    }
-
}