build: migrate to tui-realm 4.0

Upgrade tuirealm (3.x -> 4.0.0), tui-realm-stdlib (3 -> 4), tui-term
(0.2 -> 0.3). Apply all breaking changes from the 4.0 migration guide
across the termscp UI.

Key changes:

- Root-level re-exports removed; imports moved to module-qualified
  paths (`tuirealm::application`, `::component`, `::event`, `::props`,
  `::state`, `::subscription`, `::listener`, `::ratatui`). Same for
  stdlib component types (`tui_realm_stdlib::components::*`).
- `MockComponent` trait renamed to `Component`; old `Component` trait
  renamed to `AppComponent`. `#[derive(MockComponent)]` is now
  `#[derive(Component)]`. `Component::on` now takes `&Event<_>`.
- `TextSpan` replaced with `SpanStatic`/`LineStatic`/`TextStatic`
  (ratatui-based); tuple `(String, Alignment)` titles replaced with
  the new `Title` builder; `Alignment` split into
  `HorizontalAlignment`/`VerticalAlignment`; stdlib components use
  `.alignment_horizontal` instead of `.alignment`.
- `State::One`/`PropPayload::One` -> `Single`. `CmdResult::None`
  -> `NoChange`. `Props::get_or` removed; `Props::get` now returns a
  borrowed `Option<&AttrValue>` (call sites switched to
  `.and_then(AttrValue::as_*)`). `Component::query` returns
  `Option<QueryResult<'a>>`.
- `Attribute::HighlightedColor` -> `HighlightStyle` (a full `Style`).
  `.highlighted_*` helpers renamed to `.highlight_*`.
- `PollStrategy::UpTo(n)` now requires a `Duration`; tick timeout moved
  from `EventListenerCfg::poll_timeout` into `PollStrategy`.
- `TerminalBridge` removed; `Context` now holds
  `CrosstermTerminalAdapter` directly and enables raw mode + alternate
  screen explicitly. The `TerminalAdapter` trait is imported where its
  methods are used.
- `Update` trait removed; activity `update` methods are plain inherent
  functions.
- `ProgressBar` replaced by stdlib `Gauge`. Paragraph `.wrap` renamed
  to `.wrap_trim`; `.text` now takes an `Into<Text>`. Stdlib `List` row
  items are now individual lines (`Vec<Span>` per row) rather than a
  `Table` of spans; custom `FileList`/`Log` convert between the two
  models.
- Radio builders drop `.foreground(color)` so unselected items render
  with the terminal default foreground, and set
  `highlight_style(Style::default().fg(color).add_modifier(REVERSED))`
  so the selected entry is visibly highlighted only with the theme
  color.
- Custom `FileList` keeps the selected row highlighted with the full
  highlight style when focused and falls back to a foreground-only
  style when unfocused.
- Theme loading is now backwards compatible: `Theme` uses a custom
  `Deserialize` through an intermediate `ThemeFile` with optional
  fields, so missing keys, unknown values or legacy aliases
  (`transfer_progress_bar_full`/`_partial`) fall back to defaults on a
  per-field basis instead of failing the whole load.
This commit is contained in:
Christian Visintin
2026-04-18 15:26:30 +02:00
parent 2dddc88eda
commit 7fde801045
78 changed files with 2127 additions and 1588 deletions
Generated
+96 -73
View File
@@ -286,7 +286,7 @@ dependencies = [
"http 0.2.12", "http 0.2.12",
"http 1.4.0", "http 1.4.0",
"http-body 1.0.1", "http-body 1.0.1",
"lru 0.16.3", "lru",
"percent-encoding", "percent-encoding",
"regex-lite", "regex-lite",
"sha2 0.10.9", "sha2 0.10.9",
@@ -812,12 +812,6 @@ dependencies = [
"thiserror 2.0.18", "thiserror 2.0.18",
] ]
[[package]]
name = "cassowary"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]] [[package]]
name = "castaway" name = "castaway"
version = "0.2.4" version = "0.2.4"
@@ -912,9 +906,9 @@ checksum = "5417da527aa9bf6a1e10a781231effd1edd3ee82f27d5f8529ac9b279babce96"
[[package]] [[package]]
name = "compact_str" name = "compact_str"
version = "0.8.1" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" checksum = "3fdb1325a1cece981e8a296ab8f0f9b63ae357bd0784a9faaf548cc7b480707a"
dependencies = [ dependencies = [
"castaway", "castaway",
"cfg-if", "cfg-if",
@@ -1094,22 +1088,6 @@ version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crossterm"
version = "0.28.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6"
dependencies = [
"bitflags 2.11.0",
"crossterm_winapi",
"mio",
"parking_lot",
"rustix 0.38.44",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]] [[package]]
name = "crossterm" name = "crossterm"
version = "0.29.0" version = "0.29.0"
@@ -2101,8 +2079,6 @@ version = "0.15.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
dependencies = [ dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.1.5", "foldhash 0.1.5",
] ]
@@ -2448,7 +2424,7 @@ dependencies = [
"libc", "libc",
"percent-encoding", "percent-encoding",
"pin-project-lite", "pin-project-lite",
"socket2 0.5.10", "socket2 0.6.3",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",
@@ -2737,9 +2713,9 @@ dependencies = [
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.13.0" version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [ dependencies = [
"either", "either",
] ]
@@ -2798,6 +2774,17 @@ dependencies = [
"serde_json", "serde_json",
] ]
[[package]]
name = "kasuari"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899"
dependencies = [
"hashbrown 0.16.1",
"portable-atomic",
"thiserror 2.0.18",
]
[[package]] [[package]]
name = "keyring" name = "keyring"
version = "3.6.3" version = "3.6.3"
@@ -3078,6 +3065,15 @@ dependencies = [
"vcpkg", "vcpkg",
] ]
[[package]]
name = "line-clipping"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8"
dependencies = [
"bitflags 2.11.0",
]
[[package]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
version = "0.4.15" version = "0.4.15"
@@ -3117,15 +3113,6 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "lru"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
dependencies = [
"hashbrown 0.15.5",
]
[[package]] [[package]]
name = "lru" name = "lru"
version = "0.16.3" version = "0.16.3"
@@ -3682,12 +3669,6 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]] [[package]]
name = "pastey" name = "pastey"
version = "0.1.1" version = "0.1.1"
@@ -4086,7 +4067,7 @@ dependencies = [
"quinn-udp", "quinn-udp",
"rustc-hash", "rustc-hash",
"rustls 0.23.37", "rustls 0.23.37",
"socket2 0.5.10", "socket2 0.6.3",
"thiserror 2.0.18", "thiserror 2.0.18",
"tokio", "tokio",
"tracing", "tracing",
@@ -4123,7 +4104,7 @@ dependencies = [
"cfg_aliases", "cfg_aliases",
"libc", "libc",
"once_cell", "once_cell",
"socket2 0.5.10", "socket2 0.6.3",
"tracing", "tracing",
"windows-sys 0.60.2", "windows-sys 0.60.2",
] ]
@@ -4216,22 +4197,64 @@ checksum = "f66ee92bc15280519ef199a274fe0cafff4245d31bc39aaa31c011ad56cb1f05"
[[package]] [[package]]
name = "ratatui" name = "ratatui"
version = "0.29.0" version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc"
dependencies = [
"instability",
"ratatui-core",
"ratatui-crossterm",
"ratatui-widgets",
]
[[package]]
name = "ratatui-core"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
"cassowary",
"compact_str", "compact_str",
"crossterm 0.28.1", "hashbrown 0.16.1",
"indoc",
"itertools",
"kasuari",
"lru",
"strum",
"thiserror 2.0.18",
"unicode-segmentation",
"unicode-truncate",
"unicode-width 0.2.0",
]
[[package]]
name = "ratatui-crossterm"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3"
dependencies = [
"cfg-if",
"crossterm",
"instability",
"ratatui-core",
]
[[package]]
name = "ratatui-widgets"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db"
dependencies = [
"bitflags 2.11.0",
"hashbrown 0.16.1",
"indoc", "indoc",
"instability", "instability",
"itertools", "itertools",
"lru 0.12.5", "line-clipping",
"paste", "ratatui-core",
"strum", "strum",
"time",
"unicode-segmentation", "unicode-segmentation",
"unicode-truncate",
"unicode-width 0.2.0", "unicode-width 0.2.0",
] ]
@@ -5577,23 +5600,22 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "strum" name = "strum"
version = "0.26.3" version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [ dependencies = [
"strum_macros", "strum_macros",
] ]
[[package]] [[package]]
name = "strum_macros" name = "strum_macros"
version = "0.26.4" version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
"quote", "quote",
"rustversion",
"syn", "syn",
] ]
@@ -6184,9 +6206,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]] [[package]]
name = "tui-realm-stdlib" name = "tui-realm-stdlib"
version = "3.1.0" version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "333d8b8da2b3479a68fdf4bd2621f888fa869501d6c5c72bac940d0eeb19b083" checksum = "16a0e19fc8b6ea3a7a88d51ae8df4d657fb4f9a1cdf79f354217e5136838d5dd"
dependencies = [ dependencies = [
"textwrap", "textwrap",
"tuirealm", "tuirealm",
@@ -6195,22 +6217,23 @@ dependencies = [
[[package]] [[package]]
name = "tui-term" name = "tui-term"
version = "0.2.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72af159125ce32b02ceaced6cffae6394b0e6b6dfd4dc164a6c59a2db9b3c0b0" checksum = "f740cf128bd2ecffea958989646e79a546c45772dac6e42c0b86451185ec6933"
dependencies = [ dependencies = [
"ratatui", "ratatui-core",
"ratatui-widgets",
"vt100", "vt100",
] ]
[[package]] [[package]]
name = "tuirealm" name = "tuirealm"
version = "3.3.0" version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3a96abaf33552e9e2487bf56f537e49fad1d1fbc55a3fd542449d4e30e7c8f3" checksum = "257b2348be6fc22fad4b6ba6288207016d88417c071fc96268b5865e24662305"
dependencies = [ dependencies = [
"bitflags 2.11.0", "bitflags 2.11.0",
"crossterm 0.29.0", "crossterm",
"dyn-clone", "dyn-clone",
"lazy-regex", "lazy-regex",
"ratatui", "ratatui",
@@ -6220,9 +6243,9 @@ dependencies = [
[[package]] [[package]]
name = "tuirealm_derive" name = "tuirealm_derive"
version = "2.0.1" version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bad3c151090da5a036321776209b3d026df637d7f52c0984ff8d45927326ab4f" checksum = "19eb242ebcb03507baa4a13392bdfc019c54dbb74492fe5e8b75d1b69ea78ecf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -6279,13 +6302,13 @@ checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]] [[package]]
name = "unicode-truncate" name = "unicode-truncate"
version = "1.1.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5"
dependencies = [ dependencies = [
"itertools", "itertools",
"unicode-segmentation", "unicode-segmentation",
"unicode-width 0.1.14", "unicode-width 0.2.0",
] ]
[[package]] [[package]]
@@ -6721,7 +6744,7 @@ version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
dependencies = [ dependencies = [
"windows-sys 0.48.0", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
+3 -3
View File
@@ -86,9 +86,9 @@ tempfile = "3"
thiserror = "2" thiserror = "2"
tokio = { version = "1", features = ["rt"] } tokio = { version = "1", features = ["rt"] }
toml = "1" toml = "1"
tui-realm-stdlib = "3" tui-realm-stdlib = "4"
tui-term = "0.2" tui-term = "0.3"
tuirealm = "3" tuirealm = "4"
unicode-width = "0.2" unicode-width = "0.2"
whoami = "2" whoami = "2"
wildmatch = "2" wildmatch = "2"
+3 -1
View File
@@ -611,10 +611,12 @@ mod tests {
toml_file.as_file().sync_all().unwrap(); toml_file.as_file().sync_all().unwrap();
toml_file.as_file().rewind().unwrap(); toml_file.as_file().rewind().unwrap();
assert!(deserialize::<Theme>(Box::new(toml_file)).is_ok()); assert!(deserialize::<Theme>(Box::new(toml_file)).is_ok());
// Malformed theme files must still load successfully; unknown or invalid
// fields fall back to defaults so user themes remain backwards compatible.
let toml_file = create_bad_toml_theme(); let toml_file = create_bad_toml_theme();
toml_file.as_file().sync_all().unwrap(); toml_file.as_file().sync_all().unwrap();
toml_file.as_file().rewind().unwrap(); toml_file.as_file().rewind().unwrap();
assert!(deserialize::<Theme>(Box::new(toml_file)).is_err()); assert!(deserialize::<Theme>(Box::new(toml_file)).is_ok());
} }
#[test] #[test]
+205 -118
View File
@@ -12,142 +12,62 @@ use crate::utils::fmt::fmt_color;
use crate::utils::parser::parse_color; use crate::utils::parser::parse_color;
/// Theme contains all the colors lookup table for termscp /// Theme contains all the colors lookup table for termscp
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Theme { pub struct Theme {
// -- auth // -- auth
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_address: Color, pub auth_address: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_bookmarks: Color, pub auth_bookmarks: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_password: Color, pub auth_password: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_port: Color, pub auth_port: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_protocol: Color, pub auth_protocol: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_recents: Color, pub auth_recents: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub auth_username: Color, pub auth_username: Color,
// -- misc // -- misc
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_error_dialog: Color, pub misc_error_dialog: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_info_dialog: Color, pub misc_info_dialog: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_input_dialog: Color, pub misc_input_dialog: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_keys: Color, pub misc_keys: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_quit_dialog: Color, pub misc_quit_dialog: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_save_dialog: Color, pub misc_save_dialog: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub misc_warn_dialog: Color, pub misc_warn_dialog: Color,
// -- transfer // -- transfer
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_local_explorer_background: Color, pub transfer_local_explorer_background: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_local_explorer_foreground: Color, pub transfer_local_explorer_foreground: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_local_explorer_highlighted: Color, pub transfer_local_explorer_highlighted: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_log_background: Color, pub transfer_log_background: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_log_window: Color, pub transfer_log_window: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color",
alias = "transfer_progress_bar_full",
alias = "transfer_progress_bar_partial"
)]
pub transfer_progress_bar: Color, pub transfer_progress_bar: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_remote_explorer_background: Color, pub transfer_remote_explorer_background: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_remote_explorer_foreground: Color, pub transfer_remote_explorer_foreground: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_remote_explorer_highlighted: Color, pub transfer_remote_explorer_highlighted: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_status_hidden: Color, pub transfer_status_hidden: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_status_sorting: Color, pub transfer_status_sorting: Color,
#[serde( #[serde(serialize_with = "serialize_color")]
deserialize_with = "deserialize_color",
serialize_with = "serialize_color"
)]
pub transfer_status_sync_browsing: Color, pub transfer_status_sync_browsing: Color,
} }
@@ -185,16 +105,124 @@ impl Default for Theme {
} }
// -- deserializer // -- deserializer
//
// Custom deserialization: every field is optional and falls back to `Theme::default()`
// when missing or when the supplied color string is invalid. This keeps user themes
// backwards compatible even when fields are added, renamed, or contain typos.
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error> #[derive(Deserialize, Default)]
where #[serde(default)]
D: Deserializer<'de>, struct ThemeFile {
{ auth_address: Option<String>,
let s: String = Deserialize::deserialize(deserializer)?; auth_bookmarks: Option<String>,
// Parse color auth_password: Option<String>,
match parse_color(&s) { auth_port: Option<String>,
None => Err(DeError::custom("Invalid color")), auth_protocol: Option<String>,
Some(color) => Ok(color), auth_recents: Option<String>,
auth_username: Option<String>,
misc_error_dialog: Option<String>,
misc_info_dialog: Option<String>,
misc_input_dialog: Option<String>,
misc_keys: Option<String>,
misc_quit_dialog: Option<String>,
misc_save_dialog: Option<String>,
misc_warn_dialog: Option<String>,
transfer_local_explorer_background: Option<String>,
transfer_local_explorer_foreground: Option<String>,
transfer_local_explorer_highlighted: Option<String>,
transfer_log_background: Option<String>,
transfer_log_window: Option<String>,
transfer_progress_bar: Option<String>,
// Legacy aliases for the now-consolidated `transfer_progress_bar` field.
transfer_progress_bar_full: Option<String>,
transfer_progress_bar_partial: Option<String>,
transfer_remote_explorer_background: Option<String>,
transfer_remote_explorer_foreground: Option<String>,
transfer_remote_explorer_highlighted: Option<String>,
transfer_status_hidden: Option<String>,
transfer_status_sorting: Option<String>,
transfer_status_sync_browsing: Option<String>,
}
impl ThemeFile {
fn into_theme(self) -> Theme {
let defaults = Theme::default();
fn pick(value: Option<String>, fallback: Color) -> Color {
value.as_deref().and_then(parse_color).unwrap_or(fallback)
}
Theme {
auth_address: pick(self.auth_address, defaults.auth_address),
auth_bookmarks: pick(self.auth_bookmarks, defaults.auth_bookmarks),
auth_password: pick(self.auth_password, defaults.auth_password),
auth_port: pick(self.auth_port, defaults.auth_port),
auth_protocol: pick(self.auth_protocol, defaults.auth_protocol),
auth_recents: pick(self.auth_recents, defaults.auth_recents),
auth_username: pick(self.auth_username, defaults.auth_username),
misc_error_dialog: pick(self.misc_error_dialog, defaults.misc_error_dialog),
misc_info_dialog: pick(self.misc_info_dialog, defaults.misc_info_dialog),
misc_input_dialog: pick(self.misc_input_dialog, defaults.misc_input_dialog),
misc_keys: pick(self.misc_keys, defaults.misc_keys),
misc_quit_dialog: pick(self.misc_quit_dialog, defaults.misc_quit_dialog),
misc_save_dialog: pick(self.misc_save_dialog, defaults.misc_save_dialog),
misc_warn_dialog: pick(self.misc_warn_dialog, defaults.misc_warn_dialog),
transfer_local_explorer_background: pick(
self.transfer_local_explorer_background,
defaults.transfer_local_explorer_background,
),
transfer_local_explorer_foreground: pick(
self.transfer_local_explorer_foreground,
defaults.transfer_local_explorer_foreground,
),
transfer_local_explorer_highlighted: pick(
self.transfer_local_explorer_highlighted,
defaults.transfer_local_explorer_highlighted,
),
transfer_log_background: pick(
self.transfer_log_background,
defaults.transfer_log_background,
),
transfer_log_window: pick(self.transfer_log_window, defaults.transfer_log_window),
transfer_progress_bar: pick(
self.transfer_progress_bar
.or(self.transfer_progress_bar_full)
.or(self.transfer_progress_bar_partial),
defaults.transfer_progress_bar,
),
transfer_remote_explorer_background: pick(
self.transfer_remote_explorer_background,
defaults.transfer_remote_explorer_background,
),
transfer_remote_explorer_foreground: pick(
self.transfer_remote_explorer_foreground,
defaults.transfer_remote_explorer_foreground,
),
transfer_remote_explorer_highlighted: pick(
self.transfer_remote_explorer_highlighted,
defaults.transfer_remote_explorer_highlighted,
),
transfer_status_hidden: pick(
self.transfer_status_hidden,
defaults.transfer_status_hidden,
),
transfer_status_sorting: pick(
self.transfer_status_sorting,
defaults.transfer_status_sorting,
),
transfer_status_sync_browsing: pick(
self.transfer_status_sync_browsing,
defaults.transfer_status_sync_browsing,
),
}
}
}
impl<'de> Deserialize<'de> for Theme {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let file = ThemeFile::deserialize(deserializer)?;
Ok(file.into_theme())
} }
} }
@@ -207,14 +235,29 @@ where
serializer.serialize_str(s.as_str()) serializer.serialize_str(s.as_str())
} }
// Kept for backwards compatibility with any external callers; no longer used directly
// by serde derive because `Theme` now uses a custom `Deserialize` impl.
#[allow(dead_code)]
fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
match parse_color(&s) {
None => Err(DeError::custom("Invalid color")),
Some(color) => Ok(color),
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod tests {
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
use super::*; use super::*;
#[test] #[test]
fn test_config_themes_default() { fn should_get_default_theme() {
let theme: Theme = Theme::default(); let theme: Theme = Theme::default();
assert_eq!(theme.auth_address, Color::Yellow); assert_eq!(theme.auth_address, Color::Yellow);
assert_eq!(theme.auth_bookmarks, Color::LightGreen); assert_eq!(theme.auth_bookmarks, Color::LightGreen);
@@ -243,4 +286,48 @@ mod test {
assert_eq!(theme.transfer_status_sorting, Color::LightYellow); assert_eq!(theme.transfer_status_sorting, Color::LightYellow);
assert_eq!(theme.transfer_status_sync_browsing, Color::LightGreen); assert_eq!(theme.transfer_status_sync_browsing, Color::LightGreen);
} }
#[test]
fn should_accept_legacy_progress_bar_fields() {
let toml = r#"
auth_protocol = "Yellow"
transfer_progress_bar_full = "Green"
"#;
let theme: Theme = toml::from_str(toml).expect("theme should load");
assert_eq!(theme.auth_protocol, Color::Yellow);
assert_eq!(theme.transfer_progress_bar, Color::Green);
}
#[test]
fn should_ignore_duplicated_legacy_progress_bar_fields() {
let toml = r#"
auth_protocol = "Yellow"
transfer_progress_bar_full = "Green"
transfer_progress_bar_partial = "Red"
"#;
let theme: Theme = toml::from_str(toml).expect("theme should load");
assert_eq!(theme.auth_protocol, Color::Yellow);
// `_full` wins because `transfer_progress_bar` and `_full` are checked first.
assert_eq!(theme.transfer_progress_bar, Color::Green);
}
#[test]
fn should_fall_back_to_defaults_on_invalid_values() {
// Invalid color value should make *that* field fall back to default, without
// breaking the rest of the theme.
let toml = r##"
auth_protocol = "not-a-color"
auth_username = "#ca9ee6"
"##;
let theme: Theme = toml::from_str(toml).expect("theme should load");
assert_eq!(theme.auth_protocol, Theme::default().auth_protocol);
assert_eq!(theme.auth_username, Color::Rgb(202, 158, 230));
}
#[test]
fn should_fall_back_to_defaults_on_missing_fields() {
// Empty file should still produce a theme equal to default.
let theme: Theme = toml::from_str("").expect("theme should load");
assert_eq!(theme, Theme::default());
}
} }
+8 -6
View File
@@ -13,9 +13,10 @@ mod view;
// Includes // Includes
use std::time::Duration; use std::time::Duration;
use tuirealm::application::PollStrategy; use tuirealm::application::{Application, PollStrategy};
use tuirealm::event::NoUserEvent;
use tuirealm::listener::EventListenerCfg; use tuirealm::listener::EventListenerCfg;
use tuirealm::{Application, NoUserEvent, Update}; use tuirealm::terminal::TerminalAdapter;
use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason}; use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason};
use crate::config::themes::Theme; use crate::config::themes::Theme;
@@ -254,9 +255,7 @@ impl AuthActivity {
pub fn new(ticks: Duration) -> AuthActivity { pub fn new(ticks: Duration) -> AuthActivity {
AuthActivity { AuthActivity {
app: Application::init( app: Application::init(
EventListenerCfg::default() EventListenerCfg::default().crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL)
.poll_timeout(ticks),
), ),
context: None, context: None,
bookmarks_list: Vec::new(), bookmarks_list: Vec::new(),
@@ -369,7 +368,10 @@ impl Activity for AuthActivity {
return; return;
} }
// Tick // Tick
match self.app.tick(PollStrategy::UpTo(3)) { match self
.app
.tick(PollStrategy::UpTo(3, std::time::Duration::from_millis(10)))
{
Ok(messages) => { Ok(messages) => {
for msg in messages.into_iter() { for msg in messages.into_iter() {
let mut msg = Some(msg); let mut msg = Some(msg);
+5 -5
View File
@@ -28,19 +28,19 @@ pub use popup::{
WindowSizeError, WindowSizeError,
}; };
pub use text::{HelpFooter, NewVersionDisclaimer, Subtitle, Title}; pub use text::{HelpFooter, NewVersionDisclaimer, Subtitle, Title};
use tui_realm_stdlib::Phantom; use tui_realm_stdlib::components::Phantom;
use tuirealm::component::{AppComponent, Component};
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, MockComponent};
// -- global listener // -- global listener
#[derive(Default, MockComponent)] #[derive(Default, Component)]
pub struct GlobalListener { pub struct GlobalListener {
component: Phantom, component: Phantom,
} }
impl Component<Msg, NoUserEvent> for GlobalListener { impl AppComponent<Msg, NoUserEvent> for GlobalListener {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Function(10), code: Key::Esc | Key::Function(10),
+63 -42
View File
@@ -2,18 +2,22 @@
//! //!
//! auth activity bookmarks components //! auth activity bookmarks components
use tui_realm_stdlib::{Input, List, Radio}; use tui_realm_stdlib::components::{Input, List, Radio};
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, InputType, TextSpan}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{
BorderSides, BorderType, Borders, Color, HorizontalAlignment, InputType, SpanStatic, Style,
TextModifiers, Title,
};
use tuirealm::state::{State, StateValue};
use super::{FormMsg, Msg, UiMsg}; use super::{FormMsg, Msg, UiMsg};
use crate::ui::activities::auth::FormTab; use crate::ui::activities::auth::FormTab;
// -- bookmark list // -- bookmark list
#[derive(MockComponent)] #[derive(Component)]
pub struct BookmarksList { pub struct BookmarksList {
component: List, component: List,
} }
@@ -23,23 +27,23 @@ impl BookmarksList {
Self { Self {
component: List::default() component: List::default()
.borders(Borders::default().color(color).modifiers(BorderType::Plain)) .borders(Borders::default().color(color).modifiers(BorderType::Plain))
.highlighted_color(color) .highlight_style(Style::default().fg(color))
.rewind(true) .rewind(true)
.scroll(true) .scroll(true)
.step(4) .step(4)
.title("Bookmarks", Alignment::Left) .title(Title::from("Bookmarks").alignment(HorizontalAlignment::Left))
.rows( .rows(
bookmarks bookmarks
.iter() .iter()
.map(|x| vec![TextSpan::from(x.as_str())]) .map(|x| vec![SpanStatic::from(x.clone())])
.collect(), .collect::<Vec<Vec<SpanStatic>>>(),
), ),
} }
} }
} }
impl Component<Msg, NoUserEvent> for BookmarksList { impl AppComponent<Msg, NoUserEvent> for BookmarksList {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Down, .. code: Key::Down, ..
@@ -77,7 +81,7 @@ impl Component<Msg, NoUserEvent> for BookmarksList {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.state() { }) => match self.state() {
State::One(StateValue::Usize(choice)) => { State::Single(StateValue::Usize(choice)) => {
Some(Msg::Form(FormMsg::LoadBookmark(choice))) Some(Msg::Form(FormMsg::LoadBookmark(choice)))
} }
_ => Some(Msg::None), _ => Some(Msg::None),
@@ -98,7 +102,7 @@ impl Component<Msg, NoUserEvent> for BookmarksList {
// -- recents list // -- recents list
#[derive(MockComponent)] #[derive(Component)]
pub struct RecentsList { pub struct RecentsList {
component: List, component: List,
} }
@@ -108,23 +112,23 @@ impl RecentsList {
Self { Self {
component: List::default() component: List::default()
.borders(Borders::default().color(color).modifiers(BorderType::Plain)) .borders(Borders::default().color(color).modifiers(BorderType::Plain))
.highlighted_color(color) .highlight_style(Style::default().fg(color))
.rewind(true) .rewind(true)
.scroll(true) .scroll(true)
.step(4) .step(4)
.title("Recent connections", Alignment::Left) .title(Title::from("Recent connections").alignment(HorizontalAlignment::Left))
.rows( .rows(
bookmarks bookmarks
.iter() .iter()
.map(|x| vec![TextSpan::from(x.as_str())]) .map(|x| vec![SpanStatic::from(x.clone())])
.collect(), .collect::<Vec<Vec<SpanStatic>>>(),
), ),
} }
} }
} }
impl Component<Msg, NoUserEvent> for RecentsList { impl AppComponent<Msg, NoUserEvent> for RecentsList {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Down, .. code: Key::Down, ..
@@ -162,7 +166,7 @@ impl Component<Msg, NoUserEvent> for RecentsList {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.state() { }) => match self.state() {
State::One(StateValue::Usize(choice)) => { State::Single(StateValue::Usize(choice)) => {
Some(Msg::Form(FormMsg::LoadRecent(choice))) Some(Msg::Form(FormMsg::LoadRecent(choice)))
} }
_ => Some(Msg::None), _ => Some(Msg::None),
@@ -183,7 +187,7 @@ impl Component<Msg, NoUserEvent> for RecentsList {
// -- delete bookmark // -- delete bookmark
#[derive(MockComponent)] #[derive(Component)]
pub struct DeleteBookmarkPopup { pub struct DeleteBookmarkPopup {
component: Radio, component: Radio,
} }
@@ -192,6 +196,11 @@ impl DeleteBookmarkPopup {
pub fn new(color: Color) -> Self { pub fn new(color: Color) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
@@ -200,14 +209,15 @@ impl DeleteBookmarkPopup {
.choices(["Yes", "No"]) .choices(["Yes", "No"])
.value(1) .value(1)
.rewind(true) .rewind(true)
.foreground(color) .title(
.title("Delete selected bookmark?", Alignment::Center), Title::from("Delete selected bookmark?").alignment(HorizontalAlignment::Center),
),
} }
} }
} }
impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup { impl AppComponent<Msg, NoUserEvent> for DeleteBookmarkPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseDeleteBookmark)) Some(Msg::Ui(UiMsg::CloseDeleteBookmark))
@@ -237,7 +247,7 @@ impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup {
}) => { }) => {
if matches!( if matches!(
self.perform(Cmd::Submit), self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0))) CmdResult::Submit(State::Single(StateValue::Usize(0)))
) { ) {
Some(Msg::Form(FormMsg::DeleteBookmark)) Some(Msg::Form(FormMsg::DeleteBookmark))
} else { } else {
@@ -251,7 +261,7 @@ impl Component<Msg, NoUserEvent> for DeleteBookmarkPopup {
// -- delete recent // -- delete recent
#[derive(MockComponent)] #[derive(Component)]
pub struct DeleteRecentPopup { pub struct DeleteRecentPopup {
component: Radio, component: Radio,
} }
@@ -260,6 +270,11 @@ impl DeleteRecentPopup {
pub fn new(color: Color) -> Self { pub fn new(color: Color) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
@@ -268,14 +283,16 @@ impl DeleteRecentPopup {
.choices(["Yes", "No"]) .choices(["Yes", "No"])
.value(1) .value(1)
.rewind(true) .rewind(true)
.foreground(color) .title(
.title("Delete selected recent host?", Alignment::Center), Title::from("Delete selected recent host?")
.alignment(HorizontalAlignment::Center),
),
} }
} }
} }
impl Component<Msg, NoUserEvent> for DeleteRecentPopup { impl AppComponent<Msg, NoUserEvent> for DeleteRecentPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseDeleteRecent)) Some(Msg::Ui(UiMsg::CloseDeleteRecent))
@@ -305,7 +322,7 @@ impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
}) => { }) => {
if matches!( if matches!(
self.perform(Cmd::Submit), self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0))) CmdResult::Submit(State::Single(StateValue::Usize(0)))
) { ) {
Some(Msg::Form(FormMsg::DeleteRecent)) Some(Msg::Form(FormMsg::DeleteRecent))
} else { } else {
@@ -321,7 +338,7 @@ impl Component<Msg, NoUserEvent> for DeleteRecentPopup {
// -- save password // -- save password
#[derive(MockComponent)] #[derive(Component)]
pub struct BookmarkSavePassword { pub struct BookmarkSavePassword {
component: Radio, component: Radio,
form_tab: FormTab, form_tab: FormTab,
@@ -331,6 +348,11 @@ impl BookmarkSavePassword {
pub fn new(form_tab: FormTab, color: Color) -> Self { pub fn new(form_tab: FormTab, color: Color) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(Color::Reset) .color(Color::Reset)
@@ -340,15 +362,14 @@ impl BookmarkSavePassword {
.choices(["Yes", "No"]) .choices(["Yes", "No"])
.value(0) .value(0)
.rewind(true) .rewind(true)
.foreground(color) .title(Title::from("Save secrets?").alignment(HorizontalAlignment::Center)),
.title("Save secrets?", Alignment::Center),
form_tab, form_tab,
} }
} }
} }
impl Component<Msg, NoUserEvent> for BookmarkSavePassword { impl AppComponent<Msg, NoUserEvent> for BookmarkSavePassword {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseSaveBookmark)) Some(Msg::Ui(UiMsg::CloseSaveBookmark))
@@ -378,7 +399,7 @@ impl Component<Msg, NoUserEvent> for BookmarkSavePassword {
// -- new bookmark name // -- new bookmark name
#[derive(MockComponent)] #[derive(Component)]
pub struct BookmarkName { pub struct BookmarkName {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -395,15 +416,15 @@ impl BookmarkName {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.title("Bookmark name", Alignment::Left) .title(Title::from("Bookmark name").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text), .input_type(InputType::Text),
form_tab, form_tab,
} }
} }
} }
impl Component<Msg, NoUserEvent> for BookmarkName { impl AppComponent<Msg, NoUserEvent> for BookmarkName {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseSaveBookmark)) Some(Msg::Ui(UiMsg::CloseSaveBookmark))
@@ -447,7 +468,7 @@ impl Component<Msg, NoUserEvent> for BookmarkName {
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
self.perform(Cmd::Type(ch)); self.perform(Cmd::Type(*ch));
Some(Msg::None) Some(Msg::None)
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
+8 -6
View File
@@ -2,11 +2,13 @@
//! //!
//! auth activity components for file transfer params form //! auth activity components for file transfer params form
use tui_realm_stdlib::{Input, Radio}; use tui_realm_stdlib::components::{Input, Radio};
use tuirealm::Component;
use tuirealm::command::{Cmd, Direction, Position}; use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::component::AppComponent;
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; use tuirealm::props::{
BorderType, Borders, Color, HorizontalAlignment, InputType, Style, TextModifiers, Title,
};
use super::{FileTransferProtocol, FormMsg, Msg, UiMsg}; use super::{FileTransferProtocol, FormMsg, Msg, UiMsg};
use crate::ui::activities::auth::{ use crate::ui::activities::auth::{
@@ -51,8 +53,8 @@ pub use smb::InputSmbWorkgroup;
pub use webdav::InputWebDAVUri; pub use webdav::InputWebDAVUri;
fn handle_input_ev( fn handle_input_ev(
component: &mut dyn Component<Msg, NoUserEvent>, component: &mut dyn AppComponent<Msg, NoUserEvent>,
ev: Event<NoUserEvent>, ev: &Event<NoUserEvent>,
on_key_down: Msg, on_key_down: Msg,
on_key_up: Msg, on_key_up: Msg,
form_tab: FormTab, form_tab: FormTab,
@@ -104,7 +106,7 @@ fn handle_input_ev(
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
component.perform(Cmd::Type(ch)); component.perform(Cmd::Type(*ch));
Some(Msg::None) Some(Msg::None)
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
@@ -1,9 +1,9 @@
use tuirealm::component::{AppComponent, Component};
use tuirealm::event::NoUserEvent; use tuirealm::event::NoUserEvent;
use tuirealm::{Component, MockComponent};
use super::*; use super::*;
#[derive(MockComponent)] #[derive(Component)]
pub struct InputAddress { pub struct InputAddress {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -19,8 +19,11 @@ impl InputAddress {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder("127.0.0.1", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title("Remote host", Alignment::Left) "127.0.0.1",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from("Remote host").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text) .input_type(InputType::Text)
.value(host), .value(host),
form_tab, form_tab,
@@ -28,8 +31,8 @@ impl InputAddress {
} }
} }
impl Component<Msg, NoUserEvent> for InputAddress { impl AppComponent<Msg, NoUserEvent> for InputAddress {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::AddressBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::AddressBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::AddressBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::AddressBlurDown)),
@@ -44,7 +47,7 @@ impl Component<Msg, NoUserEvent> for InputAddress {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputPort { pub struct InputPort {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -60,18 +63,21 @@ impl InputPort {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder("22", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
"22",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.input_type(InputType::UnsignedInteger) .input_type(InputType::UnsignedInteger)
.input_len(5) .input_len(5)
.title("Port number", Alignment::Left) .title(Title::from("Port number").alignment(HorizontalAlignment::Left))
.value(port.to_string()), .value(port.to_string()),
form_tab, form_tab,
} }
} }
} }
impl Component<Msg, NoUserEvent> for InputPort { impl AppComponent<Msg, NoUserEvent> for InputPort {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PortBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PortBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PortBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PortBlurDown)),
@@ -86,7 +92,7 @@ impl Component<Msg, NoUserEvent> for InputPort {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputUsername { pub struct InputUsername {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -102,8 +108,11 @@ impl InputUsername {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder("root", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title("Username", Alignment::Left) "root",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from("Username").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text) .input_type(InputType::Text)
.value(username), .value(username),
form_tab, form_tab,
@@ -111,8 +120,8 @@ impl InputUsername {
} }
} }
impl Component<Msg, NoUserEvent> for InputUsername { impl AppComponent<Msg, NoUserEvent> for InputUsername {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::UsernameBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::UsernameBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::UsernameBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::UsernameBlurDown)),
@@ -127,7 +136,7 @@ impl Component<Msg, NoUserEvent> for InputUsername {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputPassword { pub struct InputPassword {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -143,7 +152,7 @@ impl InputPassword {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.title("Password", Alignment::Left) .title(Title::from("Password").alignment(HorizontalAlignment::Left))
.input_type(InputType::Password('*')) .input_type(InputType::Password('*'))
.value(password), .value(password),
form_tab, form_tab,
@@ -151,8 +160,8 @@ impl InputPassword {
} }
} }
impl Component<Msg, NoUserEvent> for InputPassword { impl AppComponent<Msg, NoUserEvent> for InputPassword {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PasswordBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::PasswordBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::PasswordBlurDown)),
+40 -26
View File
@@ -1,9 +1,9 @@
use tuirealm::component::{AppComponent, Component};
use tuirealm::event::NoUserEvent; use tuirealm::event::NoUserEvent;
use tuirealm::{Component, MockComponent};
use super::*; use super::*;
#[derive(MockComponent)] #[derive(Component)]
pub struct InputKubeNamespace { pub struct InputKubeNamespace {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -19,8 +19,11 @@ impl InputKubeNamespace {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder("namespace", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title("Pod namespace (optional)", Alignment::Left) "namespace",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from("Pod namespace (optional)").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text) .input_type(InputType::Text)
.value(bucket), .value(bucket),
form_tab, form_tab,
@@ -28,8 +31,8 @@ impl InputKubeNamespace {
} }
} }
impl Component<Msg, NoUserEvent> for InputKubeNamespace { impl AppComponent<Msg, NoUserEvent> for InputKubeNamespace {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeNamespaceBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeNamespaceBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeNamespaceBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeNamespaceBlurDown)),
@@ -44,7 +47,7 @@ impl Component<Msg, NoUserEvent> for InputKubeNamespace {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputKubeClusterUrl { pub struct InputKubeClusterUrl {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -60,11 +63,13 @@ impl InputKubeClusterUrl {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"cluster url", "cluster url",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(
Title::from("Kube cluster url (optional)").alignment(HorizontalAlignment::Left),
) )
.title("Kube cluster url (optional)", Alignment::Left)
.input_type(InputType::Text) .input_type(InputType::Text)
.value(bucket), .value(bucket),
form_tab, form_tab,
@@ -72,8 +77,8 @@ impl InputKubeClusterUrl {
} }
} }
impl Component<Msg, NoUserEvent> for InputKubeClusterUrl { impl AppComponent<Msg, NoUserEvent> for InputKubeClusterUrl {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClusterUrlBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClusterUrlBlurDown)),
FormTab::HostBridge => { FormTab::HostBridge => {
@@ -90,7 +95,7 @@ impl Component<Msg, NoUserEvent> for InputKubeClusterUrl {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputKubeUsername { pub struct InputKubeUsername {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -106,8 +111,11 @@ impl InputKubeUsername {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder("username", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title("Kube username (optional)", Alignment::Left) "username",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from("Kube username (optional)").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text) .input_type(InputType::Text)
.value(bucket), .value(bucket),
form_tab, form_tab,
@@ -115,8 +123,8 @@ impl InputKubeUsername {
} }
} }
impl Component<Msg, NoUserEvent> for InputKubeUsername { impl AppComponent<Msg, NoUserEvent> for InputKubeUsername {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeUsernameBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeUsernameBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeUsernameBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeUsernameBlurDown)),
@@ -131,7 +139,7 @@ impl Component<Msg, NoUserEvent> for InputKubeUsername {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputKubeClientCert { pub struct InputKubeClientCert {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -147,11 +155,14 @@ impl InputKubeClientCert {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"/home/user/.kube/client.crt", "/home/user/.kube/client.crt",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(
Title::from("Kube client cert path (optional)")
.alignment(HorizontalAlignment::Left),
) )
.title("Kube client cert path (optional)", Alignment::Left)
.input_type(InputType::Text) .input_type(InputType::Text)
.value(bucket), .value(bucket),
form_tab, form_tab,
@@ -159,8 +170,8 @@ impl InputKubeClientCert {
} }
} }
impl Component<Msg, NoUserEvent> for InputKubeClientCert { impl AppComponent<Msg, NoUserEvent> for InputKubeClientCert {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientCertBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientCertBlurDown)),
FormTab::HostBridge => { FormTab::HostBridge => {
@@ -177,7 +188,7 @@ impl Component<Msg, NoUserEvent> for InputKubeClientCert {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputKubeClientKey { pub struct InputKubeClientKey {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -193,11 +204,14 @@ impl InputKubeClientKey {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"/home/user/.kube/client.key", "/home/user/.kube/client.key",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(
Title::from("Kube client key path (optional)")
.alignment(HorizontalAlignment::Left),
) )
.title("Kube client key path (optional)", Alignment::Left)
.input_type(InputType::Text) .input_type(InputType::Text)
.value(bucket), .value(bucket),
form_tab, form_tab,
@@ -205,8 +219,8 @@ impl InputKubeClientKey {
} }
} }
impl Component<Msg, NoUserEvent> for InputKubeClientKey { impl AppComponent<Msg, NoUserEvent> for InputKubeClientKey {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientKeyBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::KubeClientKeyBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientKeyBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::KubeClientKeyBlurDown)),
+23 -11
View File
@@ -1,9 +1,9 @@
use tuirealm::component::{AppComponent, Component};
use tuirealm::event::NoUserEvent; use tuirealm::event::NoUserEvent;
use tuirealm::{Component, MockComponent};
use super::*; use super::*;
#[derive(MockComponent)] #[derive(Component)]
pub struct InputRemoteDirectory { pub struct InputRemoteDirectory {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -19,8 +19,14 @@ impl InputRemoteDirectory {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder("/home/foo", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title("Default remote working directory", Alignment::Left) "/home/foo",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(
Title::from("Default remote working directory")
.alignment(HorizontalAlignment::Left),
)
.input_type(InputType::Text) .input_type(InputType::Text)
.value(remote_dir), .value(remote_dir),
form_tab, form_tab,
@@ -28,8 +34,8 @@ impl InputRemoteDirectory {
} }
} }
impl Component<Msg, NoUserEvent> for InputRemoteDirectory { impl AppComponent<Msg, NoUserEvent> for InputRemoteDirectory {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::RemoteDirectoryBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::RemoteDirectoryBlurDown)),
FormTab::HostBridge => { FormTab::HostBridge => {
@@ -46,7 +52,7 @@ impl Component<Msg, NoUserEvent> for InputRemoteDirectory {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputLocalDirectory { pub struct InputLocalDirectory {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -62,8 +68,14 @@ impl InputLocalDirectory {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder("/home/foo", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title("Default local working directory", Alignment::Left) "/home/foo",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(
Title::from("Default local working directory")
.alignment(HorizontalAlignment::Left),
)
.input_type(InputType::Text) .input_type(InputType::Text)
.value(local_dir), .value(local_dir),
form_tab, form_tab,
@@ -71,8 +83,8 @@ impl InputLocalDirectory {
} }
} }
impl Component<Msg, NoUserEvent> for InputLocalDirectory { impl AppComponent<Msg, NoUserEvent> for InputLocalDirectory {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::LocalDirectoryBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::LocalDirectoryBlurDown)),
FormTab::HostBridge => { FormTab::HostBridge => {
@@ -1,11 +1,14 @@
use tuirealm::command::{Cmd, CmdResult, Direction}; use tuirealm::command::{Cmd, CmdResult, Direction};
use tuirealm::component::{AppComponent, Component};
use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::props::{Alignment, BorderType, Borders, Color}; use tuirealm::props::{
use tuirealm::{Component, MockComponent, State, StateValue}; BorderType, Borders, Color, HorizontalAlignment, Style, TextModifiers, Title,
};
use tuirealm::state::{State, StateValue};
use super::*; use super::*;
#[derive(MockComponent)] #[derive(Component)]
pub struct RemoteProtocolRadio { pub struct RemoteProtocolRadio {
component: Radio, component: Radio,
} }
@@ -14,6 +17,11 @@ impl RemoteProtocolRadio {
pub fn new(default_protocol: FileTransferProtocol, color: Color) -> Self { pub fn new(default_protocol: FileTransferProtocol, color: Color) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
@@ -24,9 +32,8 @@ impl RemoteProtocolRadio {
} else { } else {
vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV"].into_iter() vec!["SFTP", "SCP", "FTP", "FTPS", "S3", "Kube", "WebDAV"].into_iter()
}) })
.foreground(color)
.rewind(true) .rewind(true)
.title("Protocol", Alignment::Left) .title(Title::from("Protocol").alignment(HorizontalAlignment::Left))
.value(Self::protocol_enum_to_opt(default_protocol)), .value(Self::protocol_enum_to_opt(default_protocol)),
} }
} }
@@ -58,8 +65,8 @@ impl RemoteProtocolRadio {
} }
} }
impl Component<Msg, NoUserEvent> for RemoteProtocolRadio { impl AppComponent<Msg, NoUserEvent> for RemoteProtocolRadio {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let result = match ev { let result = match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -86,7 +93,7 @@ impl Component<Msg, NoUserEvent> for RemoteProtocolRadio {
}; };
match result { match result {
CmdResult::Changed(State::One(StateValue::Usize(choice))) => Some(Msg::Form( CmdResult::Changed(State::Single(StateValue::Usize(choice))) => Some(Msg::Form(
FormMsg::RemoteProtocolChanged(Self::protocol_opt_to_enum(choice)), FormMsg::RemoteProtocolChanged(Self::protocol_opt_to_enum(choice)),
)), )),
_ => Some(Msg::None), _ => Some(Msg::None),
@@ -94,7 +101,7 @@ impl Component<Msg, NoUserEvent> for RemoteProtocolRadio {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct HostBridgeProtocolRadio { pub struct HostBridgeProtocolRadio {
component: Radio, component: Radio,
} }
@@ -103,6 +110,11 @@ impl HostBridgeProtocolRadio {
pub fn new(protocol: HostBridgeProtocol, color: Color) -> Self { pub fn new(protocol: HostBridgeProtocol, color: Color) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
@@ -134,9 +146,8 @@ impl HostBridgeProtocolRadio {
] ]
.into_iter() .into_iter()
}) })
.foreground(color)
.rewind(true) .rewind(true)
.title("Host type", Alignment::Left) .title(Title::from("Host type").alignment(HorizontalAlignment::Left))
.value(Self::protocol_to_opt(protocol)), .value(Self::protocol_to_opt(protocol)),
} }
} }
@@ -195,8 +206,8 @@ impl HostBridgeProtocolRadio {
} }
} }
impl Component<Msg, NoUserEvent> for HostBridgeProtocolRadio { impl AppComponent<Msg, NoUserEvent> for HostBridgeProtocolRadio {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let result = match ev { let result = match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -223,7 +234,7 @@ impl Component<Msg, NoUserEvent> for HostBridgeProtocolRadio {
}; };
match result { match result {
CmdResult::Changed(State::One(StateValue::Usize(choice))) => Some(Msg::Form( CmdResult::Changed(State::Single(StateValue::Usize(choice))) => Some(Msg::Form(
FormMsg::HostBridgeProtocolChanged(Self::protocol_opt_to_enum(choice)), FormMsg::HostBridgeProtocolChanged(Self::protocol_opt_to_enum(choice)),
)), )),
_ => Some(Msg::None), _ => Some(Msg::None),
+60 -44
View File
@@ -1,10 +1,10 @@
use tuirealm::command::{Cmd, Direction}; use tuirealm::command::{Cmd, Direction};
use tuirealm::component::{AppComponent, Component};
use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{Component, MockComponent};
use super::*; use super::*;
#[derive(MockComponent)] #[derive(Component)]
pub struct InputS3Bucket { pub struct InputS3Bucket {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -20,8 +20,11 @@ impl InputS3Bucket {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder("my-bucket", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title("Bucket name", Alignment::Left) "my-bucket",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from("Bucket name").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text) .input_type(InputType::Text)
.value(bucket), .value(bucket),
form_tab, form_tab,
@@ -29,8 +32,8 @@ impl InputS3Bucket {
} }
} }
impl Component<Msg, NoUserEvent> for InputS3Bucket { impl AppComponent<Msg, NoUserEvent> for InputS3Bucket {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3BucketBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3BucketBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3BucketBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3BucketBlurDown)),
@@ -45,7 +48,7 @@ impl Component<Msg, NoUserEvent> for InputS3Bucket {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputS3Region { pub struct InputS3Region {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -61,8 +64,11 @@ impl InputS3Region {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder("eu-west-1", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title("Region", Alignment::Left) "eu-west-1",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from("Region").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text) .input_type(InputType::Text)
.value(region), .value(region),
form_tab, form_tab,
@@ -70,8 +76,8 @@ impl InputS3Region {
} }
} }
impl Component<Msg, NoUserEvent> for InputS3Region { impl AppComponent<Msg, NoUserEvent> for InputS3Region {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3RegionBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3RegionBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3RegionBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3RegionBlurDown)),
@@ -86,7 +92,7 @@ impl Component<Msg, NoUserEvent> for InputS3Region {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputS3Endpoint { pub struct InputS3Endpoint {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -102,11 +108,11 @@ impl InputS3Endpoint {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"http://localhost:9000", "http://localhost:9000",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
) ))
.title("Endpoint", Alignment::Left) .title(Title::from("Endpoint").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text) .input_type(InputType::Text)
.value(endpoint), .value(endpoint),
form_tab, form_tab,
@@ -114,8 +120,8 @@ impl InputS3Endpoint {
} }
} }
impl Component<Msg, NoUserEvent> for InputS3Endpoint { impl AppComponent<Msg, NoUserEvent> for InputS3Endpoint {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3EndpointBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3EndpointBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3EndpointBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3EndpointBlurDown)),
@@ -130,7 +136,7 @@ impl Component<Msg, NoUserEvent> for InputS3Endpoint {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct RadioS3NewPathStyle { pub struct RadioS3NewPathStyle {
component: Radio, component: Radio,
form_tab: FormTab, form_tab: FormTab,
@@ -140,23 +146,27 @@ impl RadioS3NewPathStyle {
pub fn new(new_path_style: bool, form_tab: FormTab, color: Color) -> Self { pub fn new(new_path_style: bool, form_tab: FormTab, color: Color) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.choices(["Yes", "No"]) .choices(["Yes", "No"])
.foreground(color)
.rewind(true) .rewind(true)
.title("New path style", Alignment::Left) .title(Title::from("New path style").alignment(HorizontalAlignment::Left))
.value(usize::from(!new_path_style)), .value(usize::from(!new_path_style)),
form_tab, form_tab,
} }
} }
} }
impl Component<Msg, NoUserEvent> for RadioS3NewPathStyle { impl AppComponent<Msg, NoUserEvent> for RadioS3NewPathStyle {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -199,7 +209,7 @@ impl Component<Msg, NoUserEvent> for RadioS3NewPathStyle {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputS3Profile { pub struct InputS3Profile {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -215,8 +225,11 @@ impl InputS3Profile {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder("default", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title("Profile", Alignment::Left) "default",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from("Profile").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text) .input_type(InputType::Text)
.value(profile), .value(profile),
form_tab, form_tab,
@@ -224,8 +237,8 @@ impl InputS3Profile {
} }
} }
impl Component<Msg, NoUserEvent> for InputS3Profile { impl AppComponent<Msg, NoUserEvent> for InputS3Profile {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3ProfileBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3ProfileBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3ProfileBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3ProfileBlurDown)),
@@ -240,7 +253,7 @@ impl Component<Msg, NoUserEvent> for InputS3Profile {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputS3AccessKey { pub struct InputS3AccessKey {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -256,8 +269,11 @@ impl InputS3AccessKey {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder("AKIA...", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title("Access key", Alignment::Left) "AKIA...",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from("Access key").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text) .input_type(InputType::Text)
.value(access_key), .value(access_key),
form_tab, form_tab,
@@ -265,8 +281,8 @@ impl InputS3AccessKey {
} }
} }
impl Component<Msg, NoUserEvent> for InputS3AccessKey { impl AppComponent<Msg, NoUserEvent> for InputS3AccessKey {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3AccessKeyBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3AccessKeyBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3AccessKeyBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::S3AccessKeyBlurDown)),
@@ -281,7 +297,7 @@ impl Component<Msg, NoUserEvent> for InputS3AccessKey {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputS3SecretAccessKey { pub struct InputS3SecretAccessKey {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -297,7 +313,7 @@ impl InputS3SecretAccessKey {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.title("Secret access key", Alignment::Left) .title(Title::from("Secret access key").alignment(HorizontalAlignment::Left))
.input_type(InputType::Password('*')) .input_type(InputType::Password('*'))
.value(secret_access_key), .value(secret_access_key),
form_tab, form_tab,
@@ -305,8 +321,8 @@ impl InputS3SecretAccessKey {
} }
} }
impl Component<Msg, NoUserEvent> for InputS3SecretAccessKey { impl AppComponent<Msg, NoUserEvent> for InputS3SecretAccessKey {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SecretAccessKeyBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SecretAccessKeyBlurDown)),
FormTab::HostBridge => { FormTab::HostBridge => {
@@ -325,7 +341,7 @@ impl Component<Msg, NoUserEvent> for InputS3SecretAccessKey {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputS3SecurityToken { pub struct InputS3SecurityToken {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -341,7 +357,7 @@ impl InputS3SecurityToken {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.title("Security token", Alignment::Left) .title(Title::from("Security token").alignment(HorizontalAlignment::Left))
.input_type(InputType::Password('*')) .input_type(InputType::Password('*'))
.value(security_token), .value(security_token),
form_tab, form_tab,
@@ -349,8 +365,8 @@ impl InputS3SecurityToken {
} }
} }
impl Component<Msg, NoUserEvent> for InputS3SecurityToken { impl AppComponent<Msg, NoUserEvent> for InputS3SecurityToken {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SecurityTokenBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SecurityTokenBlurDown)),
FormTab::HostBridge => { FormTab::HostBridge => {
@@ -367,7 +383,7 @@ impl Component<Msg, NoUserEvent> for InputS3SecurityToken {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct InputS3SessionToken { pub struct InputS3SessionToken {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -383,7 +399,7 @@ impl InputS3SessionToken {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.title("Session token", Alignment::Left) .title(Title::from("Session token").alignment(HorizontalAlignment::Left))
.input_type(InputType::Password('*')) .input_type(InputType::Password('*'))
.value(session_token), .value(session_token),
form_tab, form_tab,
@@ -391,8 +407,8 @@ impl InputS3SessionToken {
} }
} }
impl Component<Msg, NoUserEvent> for InputS3SessionToken { impl AppComponent<Msg, NoUserEvent> for InputS3SessionToken {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SessionTokenBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::S3SessionTokenBlurDown)),
FormTab::HostBridge => { FormTab::HostBridge => {
@@ -1,9 +1,9 @@
use tuirealm::component::{AppComponent, Component};
use tuirealm::event::NoUserEvent; use tuirealm::event::NoUserEvent;
use tuirealm::{Component, MockComponent};
use super::*; use super::*;
#[derive(MockComponent)] #[derive(Component)]
pub struct InputSmbShare { pub struct InputSmbShare {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -19,7 +19,7 @@ impl InputSmbShare {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.title("Share", Alignment::Left) .title(Title::from("Share").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text) .input_type(InputType::Text)
.value(host), .value(host),
form_tab, form_tab,
@@ -27,8 +27,8 @@ impl InputSmbShare {
} }
} }
impl Component<Msg, NoUserEvent> for InputSmbShare { impl AppComponent<Msg, NoUserEvent> for InputSmbShare {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbShareBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbShareBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbShareBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbShareBlurDown)),
@@ -44,7 +44,7 @@ impl Component<Msg, NoUserEvent> for InputSmbShare {
} }
#[cfg(posix)] #[cfg(posix)]
#[derive(MockComponent)] #[derive(Component)]
pub struct InputSmbWorkgroup { pub struct InputSmbWorkgroup {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -61,7 +61,7 @@ impl InputSmbWorkgroup {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.title("Workgroup", Alignment::Left) .title(Title::from("Workgroup").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text) .input_type(InputType::Text)
.value(host), .value(host),
form_tab, form_tab,
@@ -70,8 +70,8 @@ impl InputSmbWorkgroup {
} }
#[cfg(posix)] #[cfg(posix)]
impl Component<Msg, NoUserEvent> for InputSmbWorkgroup { impl AppComponent<Msg, NoUserEvent> for InputSmbWorkgroup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbWorkgroupDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::SmbWorkgroupDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbWorkgroupDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::SmbWorkgroupDown)),
@@ -1,9 +1,9 @@
use tuirealm::component::{AppComponent, Component};
use tuirealm::event::NoUserEvent; use tuirealm::event::NoUserEvent;
use tuirealm::{Component, MockComponent};
use super::*; use super::*;
#[derive(MockComponent)] #[derive(Component)]
pub struct InputWebDAVUri { pub struct InputWebDAVUri {
component: Input, component: Input,
form_tab: FormTab, form_tab: FormTab,
@@ -19,11 +19,11 @@ impl InputWebDAVUri {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"http://localhost:8080", "http://localhost:8080",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
) ))
.title("HTTP url", Alignment::Left) .title(Title::from("HTTP url").alignment(HorizontalAlignment::Left))
.input_type(InputType::Text) .input_type(InputType::Text)
.value(host), .value(host),
form_tab, form_tab,
@@ -31,8 +31,8 @@ impl InputWebDAVUri {
} }
} }
impl Component<Msg, NoUserEvent> for InputWebDAVUri { impl AppComponent<Msg, NoUserEvent> for InputWebDAVUri {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let on_key_down = match self.form_tab { let on_key_down = match self.form_tab {
FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::WebDAVUriBlurDown)), FormTab::Remote => Msg::Ui(UiMsg::Remote(UiAuthFormMsg::WebDAVUriBlurDown)),
FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::WebDAVUriBlurDown)), FormTab::HostBridge => Msg::Ui(UiMsg::HostBridge(UiAuthFormMsg::WebDAVUriBlurDown)),
+92 -68
View File
@@ -2,17 +2,23 @@
//! //!
//! auth activity popups //! auth activity popups
use tui_realm_stdlib::{List, Paragraph, Radio, Textarea}; use tui_realm_stdlib::components::{List, Paragraph, Radio, Textarea};
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, TableBuilder, TextSpan}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{
BorderType, Borders, Color, HorizontalAlignment, SpanStatic, Style, TableBuilder,
TextModifiers, Title,
};
use tuirealm::ratatui::style::Stylize;
use tuirealm::ratatui::text::Text;
use tuirealm::state::{State, StateValue};
use super::{FormMsg, Msg, UiMsg}; use super::{FormMsg, Msg, UiMsg};
// -- error popup // -- error popup
#[derive(MockComponent)] #[derive(Component)]
pub struct ErrorPopup { pub struct ErrorPopup {
component: Paragraph, component: Paragraph,
} }
@@ -21,21 +27,23 @@ impl ErrorPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self { pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self { Self {
component: Paragraph::default() component: Paragraph::default()
.alignment(Alignment::Center) .alignment_horizontal(HorizontalAlignment::Center)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.text([TextSpan::from(text.as_ref())]) .text(Text::from_iter([SpanStatic::from(
.wrap(true), text.as_ref().to_string(),
)]))
.wrap_trim(true),
} }
} }
} }
impl Component<Msg, NoUserEvent> for ErrorPopup { impl AppComponent<Msg, NoUserEvent> for ErrorPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter, code: Key::Esc | Key::Enter,
@@ -48,7 +56,7 @@ impl Component<Msg, NoUserEvent> for ErrorPopup {
// -- info popup // -- info popup
#[derive(MockComponent)] #[derive(Component)]
pub struct InfoPopup { pub struct InfoPopup {
component: Paragraph, component: Paragraph,
} }
@@ -57,21 +65,23 @@ impl InfoPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self { pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self { Self {
component: Paragraph::default() component: Paragraph::default()
.alignment(Alignment::Center) .alignment_horizontal(HorizontalAlignment::Center)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.text([TextSpan::from(text.as_ref())]) .text(Text::from_iter([SpanStatic::from(
.wrap(true), text.as_ref().to_string(),
)]))
.wrap_trim(true),
} }
} }
} }
impl Component<Msg, NoUserEvent> for InfoPopup { impl AppComponent<Msg, NoUserEvent> for InfoPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter, code: Key::Esc | Key::Enter,
@@ -84,7 +94,7 @@ impl Component<Msg, NoUserEvent> for InfoPopup {
// -- wait popup // -- wait popup
#[derive(MockComponent)] #[derive(Component)]
pub struct WaitPopup { pub struct WaitPopup {
component: Paragraph, component: Paragraph,
} }
@@ -93,28 +103,30 @@ impl WaitPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self { pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self { Self {
component: Paragraph::default() component: Paragraph::default()
.alignment(Alignment::Center) .alignment_horizontal(HorizontalAlignment::Center)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.text([TextSpan::from(text.as_ref())]) .text(Text::from_iter([SpanStatic::from(
.wrap(true), text.as_ref().to_string(),
)]))
.wrap_trim(true),
} }
} }
} }
impl Component<Msg, NoUserEvent> for WaitPopup { impl AppComponent<Msg, NoUserEvent> for WaitPopup {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
// -- window size error // -- window size error
#[derive(MockComponent)] #[derive(Component)]
pub struct WindowSizeError { pub struct WindowSizeError {
component: Paragraph, component: Paragraph,
} }
@@ -123,30 +135,30 @@ impl WindowSizeError {
pub fn new(color: Color) -> Self { pub fn new(color: Color) -> Self {
Self { Self {
component: Paragraph::default() component: Paragraph::default()
.alignment(Alignment::Center) .alignment_horizontal(HorizontalAlignment::Center)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.text([TextSpan::from( .text(Text::from_iter([SpanStatic::from(
"termscp requires at least 24 lines of height to run", "termscp requires at least 24 lines of height to run",
)]) )]))
.wrap(true), .wrap_trim(true),
} }
} }
} }
impl Component<Msg, NoUserEvent> for WindowSizeError { impl AppComponent<Msg, NoUserEvent> for WindowSizeError {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
// -- quit popup // -- quit popup
#[derive(MockComponent)] #[derive(Component)]
pub struct QuitPopup { pub struct QuitPopup {
component: Radio, component: Radio,
} }
@@ -155,21 +167,25 @@ impl QuitPopup {
pub fn new(color: Color) -> Self { pub fn new(color: Color) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .title(Title::from("Quit termscp?").alignment(HorizontalAlignment::Center))
.title("Quit termscp?", Alignment::Center)
.rewind(true) .rewind(true)
.choices(["Yes", "No"]), .choices(["Yes", "No"]),
} }
} }
} }
impl Component<Msg, NoUserEvent> for QuitPopup { impl AppComponent<Msg, NoUserEvent> for QuitPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseQuitPopup)) Some(Msg::Ui(UiMsg::CloseQuitPopup))
@@ -199,7 +215,7 @@ impl Component<Msg, NoUserEvent> for QuitPopup {
}) => { }) => {
if matches!( if matches!(
self.perform(Cmd::Submit), self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0))) CmdResult::Submit(State::Single(StateValue::Usize(0)))
) { ) {
Some(Msg::Form(FormMsg::Quit)) Some(Msg::Form(FormMsg::Quit))
} else { } else {
@@ -213,7 +229,7 @@ impl Component<Msg, NoUserEvent> for QuitPopup {
// -- install update popup // -- install update popup
#[derive(MockComponent)] #[derive(Component)]
pub struct InstallUpdatePopup { pub struct InstallUpdatePopup {
component: Radio, component: Radio,
} }
@@ -222,21 +238,25 @@ impl InstallUpdatePopup {
pub fn new(color: Color) -> Self { pub fn new(color: Color) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .title(Title::from("Install update?").alignment(HorizontalAlignment::Center))
.title("Install update?", Alignment::Center)
.rewind(true) .rewind(true)
.choices(["Yes", "No"]), .choices(["Yes", "No"]),
} }
} }
} }
impl Component<Msg, NoUserEvent> for InstallUpdatePopup { impl AppComponent<Msg, NoUserEvent> for InstallUpdatePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseInstallUpdatePopup)) Some(Msg::Ui(UiMsg::CloseInstallUpdatePopup))
@@ -266,7 +286,7 @@ impl Component<Msg, NoUserEvent> for InstallUpdatePopup {
}) => { }) => {
if matches!( if matches!(
self.perform(Cmd::Submit), self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0))) CmdResult::Submit(State::Single(StateValue::Usize(0)))
) { ) {
Some(Msg::Form(FormMsg::InstallUpdate)) Some(Msg::Form(FormMsg::InstallUpdate))
} else { } else {
@@ -280,7 +300,7 @@ impl Component<Msg, NoUserEvent> for InstallUpdatePopup {
// -- release notes popup // -- release notes popup
#[derive(MockComponent)] #[derive(Component)]
pub struct ReleaseNotes { pub struct ReleaseNotes {
component: Textarea, component: Textarea,
} }
@@ -295,14 +315,14 @@ impl ReleaseNotes {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.title("Release notes", Alignment::Center) .title(Title::from("Release notes").alignment(HorizontalAlignment::Center))
.text_rows(notes.lines().map(TextSpan::from)), .text_rows(notes.lines().map(|l| SpanStatic::from(l.to_string()))),
} }
} }
} }
impl Component<Msg, NoUserEvent> for ReleaseNotes { impl AppComponent<Msg, NoUserEvent> for ReleaseNotes {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter, code: Key::Esc | Key::Enter,
@@ -348,7 +368,7 @@ impl Component<Msg, NoUserEvent> for ReleaseNotes {
// -- keybindings popup // -- keybindings popup
#[derive(MockComponent)] #[derive(Component)]
pub struct Keybindings { pub struct Keybindings {
component: List, component: List,
} }
@@ -362,43 +382,47 @@ impl Keybindings {
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.highlighted_str("? ") .highlight_str("? ")
.title("Keybindings", Alignment::Center) .title(Title::from("Keybindings").alignment(HorizontalAlignment::Center))
.scroll(true) .scroll(true)
.step(4) .step(4)
.rows( .rows(
TableBuilder::default() TableBuilder::default()
.add_col(TextSpan::new("<ESC>").bold().fg(color)) .add_col(SpanStatic::raw("<ESC>").bold().fg(color))
.add_col(TextSpan::from(" Quit termscp")) .add_col(SpanStatic::from(" Quit termscp"))
.add_row() .add_row()
.add_col(TextSpan::new("<TAB>").bold().fg(color)) .add_col(SpanStatic::raw("<TAB>").bold().fg(color))
.add_col(TextSpan::from(" Switch from form and bookmarks")) .add_col(SpanStatic::from(
" Switch from form and bookmarks",
))
.add_row() .add_row()
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(color)) .add_col(SpanStatic::raw("<RIGHT/LEFT>").bold().fg(color))
.add_col(TextSpan::from(" Switch bookmark tab")) .add_col(SpanStatic::from(" Switch bookmark tab"))
.add_row() .add_row()
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(color)) .add_col(SpanStatic::raw("<UP/DOWN>").bold().fg(color))
.add_col(TextSpan::from(" Move up/down in current tab")) .add_col(SpanStatic::from(" Move up/down in current tab"))
.add_row() .add_row()
.add_col(TextSpan::new("<ENTER>").bold().fg(color)) .add_col(SpanStatic::raw("<ENTER>").bold().fg(color))
.add_col(TextSpan::from(" Connect/Load bookmark")) .add_col(SpanStatic::from(" Connect/Load bookmark"))
.add_row() .add_row()
.add_col(TextSpan::new("<DEL|E>").bold().fg(color)) .add_col(SpanStatic::raw("<DEL|E>").bold().fg(color))
.add_col(TextSpan::from(" Delete selected bookmark")) .add_col(SpanStatic::from(" Delete selected bookmark"))
.add_row() .add_row()
.add_col(TextSpan::new("<CTRL+C>").bold().fg(color)) .add_col(SpanStatic::raw("<CTRL+C>").bold().fg(color))
.add_col(TextSpan::from(" Enter setup")) .add_col(SpanStatic::from(" Enter setup"))
.add_row() .add_row()
.add_col(TextSpan::new("<CTRL+S>").bold().fg(color)) .add_col(SpanStatic::raw("<CTRL+S>").bold().fg(color))
.add_col(TextSpan::from(" Save bookmark")) .add_col(SpanStatic::from(" Save bookmark"))
.build(), .build()
.into_iter()
.map(|row| row.into_iter().flat_map(|l| l.spans).collect::<Vec<_>>()),
), ),
} }
} }
} }
impl Component<Msg, NoUserEvent> for Keybindings { impl AppComponent<Msg, NoUserEvent> for Keybindings {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter, code: Key::Esc | Key::Enter,
+34 -32
View File
@@ -2,15 +2,17 @@
//! //!
//! auth activity texts //! auth activity texts
use tui_realm_stdlib::{Label, Span}; use tui_realm_stdlib::components::{Label, Span};
use tuirealm::props::{Color, TextModifiers, TextSpan}; use tuirealm::component::{AppComponent, Component};
use tuirealm::{Component, Event, MockComponent, NoUserEvent}; use tuirealm::event::{Event, NoUserEvent};
use tuirealm::props::{Color, SpanStatic, TextModifiers};
use tuirealm::ratatui::style::Stylize;
use super::Msg; use super::Msg;
// -- Title // -- Title
#[derive(MockComponent)] #[derive(Component)]
pub struct Title { pub struct Title {
component: Label, component: Label,
} }
@@ -25,15 +27,15 @@ impl Default for Title {
} }
} }
impl Component<Msg, NoUserEvent> for Title { impl AppComponent<Msg, NoUserEvent> for Title {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
// -- subtitle // -- subtitle
#[derive(MockComponent)] #[derive(Component)]
pub struct Subtitle { pub struct Subtitle {
component: Label, component: Label,
} }
@@ -48,15 +50,15 @@ impl Default for Subtitle {
} }
} }
impl Component<Msg, NoUserEvent> for Subtitle { impl AppComponent<Msg, NoUserEvent> for Subtitle {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
// -- new version disclaimer // -- new version disclaimer
#[derive(MockComponent)] #[derive(Component)]
pub struct NewVersionDisclaimer { pub struct NewVersionDisclaimer {
component: Span, component: Span,
} }
@@ -65,9 +67,9 @@ impl NewVersionDisclaimer {
pub fn new(new_version: &str, color: Color) -> Self { pub fn new(new_version: &str, color: Color) -> Self {
Self { Self {
component: Span::default().foreground(color).spans([ component: Span::default().foreground(color).spans([
TextSpan::from("termscp "), SpanStatic::from("termscp "),
TextSpan::new(new_version).underlined().bold(), SpanStatic::raw(new_version.to_string()).underlined().bold(),
TextSpan::from( SpanStatic::from(
" is NOW available! Install update and view release notes with <CTRL+R>", " is NOW available! Install update and view release notes with <CTRL+R>",
), ),
]), ]),
@@ -75,15 +77,15 @@ impl NewVersionDisclaimer {
} }
} }
impl Component<Msg, NoUserEvent> for NewVersionDisclaimer { impl AppComponent<Msg, NoUserEvent> for NewVersionDisclaimer {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
// -- HelpFooter // -- HelpFooter
#[derive(MockComponent)] #[derive(Component)]
pub struct HelpFooter { pub struct HelpFooter {
component: Span, component: Span,
} }
@@ -92,27 +94,27 @@ impl HelpFooter {
pub fn new(key_color: Color) -> Self { pub fn new(key_color: Color) -> Self {
Self { Self {
component: Span::default().spans([ component: Span::default().spans([
TextSpan::from("<F1|CTRL+H>").bold().fg(key_color), SpanStatic::from("<F1|CTRL+H>").bold().fg(key_color),
TextSpan::from(" Help "), SpanStatic::from(" Help "),
TextSpan::from("<CTRL+C>").bold().fg(key_color), SpanStatic::from("<CTRL+C>").bold().fg(key_color),
TextSpan::from(" Enter setup "), SpanStatic::from(" Enter setup "),
TextSpan::from("<UP/DOWN>").bold().fg(key_color), SpanStatic::from("<UP/DOWN>").bold().fg(key_color),
TextSpan::from(" Change field "), SpanStatic::from(" Change field "),
TextSpan::from("<TAB>").bold().fg(key_color), SpanStatic::from("<TAB>").bold().fg(key_color),
TextSpan::from(" Switch tab "), SpanStatic::from(" Switch tab "),
TextSpan::from("<BACKTAB>").bold().fg(key_color), SpanStatic::from("<BACKTAB>").bold().fg(key_color),
TextSpan::from(" Switch form "), SpanStatic::from(" Switch form "),
TextSpan::from("<ENTER>").bold().fg(key_color), SpanStatic::from("<ENTER>").bold().fg(key_color),
TextSpan::from(" Submit form "), SpanStatic::from(" Submit form "),
TextSpan::from("<F10|ESC>").bold().fg(key_color), SpanStatic::from("<F10|ESC>").bold().fg(key_color),
TextSpan::from(" Quit "), SpanStatic::from(" Quit "),
]), ]),
} }
} }
} }
impl Component<Msg, NoUserEvent> for HelpFooter { impl AppComponent<Msg, NoUserEvent> for HelpFooter {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
+9 -6
View File
@@ -2,15 +2,15 @@
//! //!
//! Update impl //! Update impl
use tuirealm::{State, StateValue}; use tuirealm::state::{State, StateValue};
use super::{ use super::{
AuthActivity, AuthFormId, ExitReason, FormMsg, FormTab, HostBridgeProtocol, Id, InputMask, Msg, AuthActivity, AuthFormId, ExitReason, FormMsg, FormTab, HostBridgeProtocol, Id, InputMask, Msg,
UiAuthFormMsg, UiMsg, Update, UiAuthFormMsg, UiMsg,
}; };
impl Update<Msg> for AuthActivity { impl AuthActivity {
fn update(&mut self, msg: Option<Msg>) -> Option<Msg> { pub(super) fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
self.redraw = true; self.redraw = true;
match msg.unwrap_or(Msg::None) { match msg.unwrap_or(Msg::None) {
Msg::Form(msg) => self.update_form(msg), Msg::Form(msg) => self.update_form(msg),
@@ -49,7 +49,9 @@ impl AuthActivity {
self.exit_reason = Some(super::ExitReason::Connect); self.exit_reason = Some(super::ExitReason::Connect);
} }
FormMsg::DeleteBookmark => { FormMsg::DeleteBookmark => {
if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::BookmarksList) { if let Ok(State::Single(StateValue::Usize(idx))) =
self.app.state(&Id::BookmarksList)
{
// Umount dialog // Umount dialog
self.umount_bookmark_del_dialog(); self.umount_bookmark_del_dialog();
// Delete bookmark // Delete bookmark
@@ -59,7 +61,8 @@ impl AuthActivity {
} }
} }
FormMsg::DeleteRecent => { FormMsg::DeleteRecent => {
if let Ok(State::One(StateValue::Usize(idx))) = self.app.state(&Id::RecentsList) { if let Ok(State::Single(StateValue::Usize(idx))) = self.app.state(&Id::RecentsList)
{
// Umount dialog // Umount dialog
self.umount_recent_del_dialog(); self.umount_recent_del_dialog();
// Delete recent // Delete recent
+1
View File
@@ -5,6 +5,7 @@
use tuirealm::props::Color; use tuirealm::props::Color;
use tuirealm::ratatui::layout::{Constraint, Direction, Layout}; use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
use tuirealm::ratatui::widgets::Clear; use tuirealm::ratatui::widgets::Clear;
use tuirealm::terminal::TerminalAdapter;
use super::{ use super::{
AuthActivity, AuthFormId, Context, FileTransferProtocol, FormTab, HostBridgeProtocol, Id, AuthActivity, AuthFormId, Context, FileTransferProtocol, FormTab, HostBridgeProtocol, Id,
+28 -26
View File
@@ -1,7 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use tuirealm::{State, StateValue}; use tuirealm::state::{State, StateValue};
use super::*; use super::*;
use crate::filetransfer::FileTransferParams; use crate::filetransfer::FileTransferParams;
@@ -123,7 +123,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::RemoteDirectory)) .state(&Self::form_tab_id(form_tab, AuthFormId::RemoteDirectory))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => { Ok(State::Single(StateValue::String(x))) if !x.is_empty() => {
Some(PathBuf::from(x.as_str())) Some(PathBuf::from(x.as_str()))
} }
_ => None, _ => None,
@@ -138,7 +138,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::LocalDirectory)) .state(&Self::form_tab_id(form_tab, AuthFormId::LocalDirectory))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => { Ok(State::Single(StateValue::String(x))) if !x.is_empty() => {
Some(PathBuf::from(x.as_str())) Some(PathBuf::from(x.as_str()))
} }
_ => None, _ => None,
@@ -150,7 +150,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::WebDAVUri)) .state(&Self::form_tab_id(form_tab, AuthFormId::WebDAVUri))
{ {
Ok(State::One(StateValue::String(x))) => x, Ok(State::Single(StateValue::String(x))) => x,
_ => String::new(), _ => String::new(),
} }
} }
@@ -160,7 +160,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::Address)) .state(&Self::form_tab_id(form_tab, AuthFormId::Address))
{ {
Ok(State::One(StateValue::String(x))) => x, Ok(State::Single(StateValue::String(x))) => x,
_ => String::new(), _ => String::new(),
} }
} }
@@ -170,7 +170,9 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::Port)) .state(&Self::form_tab_id(form_tab, AuthFormId::Port))
{ {
Ok(State::One(StateValue::String(x))) => u16::from_str(x.as_str()).unwrap_or_default(), Ok(State::Single(StateValue::String(x))) => {
u16::from_str(x.as_str()).unwrap_or_default()
}
_ => 0, _ => 0,
} }
} }
@@ -183,7 +185,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::Username)) .state(&Self::form_tab_id(form_tab, AuthFormId::Username))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -196,7 +198,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::Password)) .state(&Self::form_tab_id(form_tab, AuthFormId::Password))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -206,7 +208,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::S3Bucket)) .state(&Self::form_tab_id(form_tab, AuthFormId::S3Bucket))
{ {
Ok(State::One(StateValue::String(x))) => x, Ok(State::Single(StateValue::String(x))) => x,
_ => String::new(), _ => String::new(),
} }
} }
@@ -219,7 +221,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::S3Region)) .state(&Self::form_tab_id(form_tab, AuthFormId::S3Region))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -232,7 +234,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::S3Endpoint)) .state(&Self::form_tab_id(form_tab, AuthFormId::S3Endpoint))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -245,7 +247,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::S3Profile)) .state(&Self::form_tab_id(form_tab, AuthFormId::S3Profile))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -258,7 +260,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::S3AccessKey)) .state(&Self::form_tab_id(form_tab, AuthFormId::S3AccessKey))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -271,7 +273,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::S3SecretAccessKey)) .state(&Self::form_tab_id(form_tab, AuthFormId::S3SecretAccessKey))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -284,7 +286,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::S3SecurityToken)) .state(&Self::form_tab_id(form_tab, AuthFormId::S3SecurityToken))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -297,7 +299,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::S3SessionToken)) .state(&Self::form_tab_id(form_tab, AuthFormId::S3SessionToken))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -309,7 +311,7 @@ impl AuthActivity {
matches!( matches!(
self.app self.app
.state(&Self::form_tab_id(form_tab, AuthFormId::S3NewPathStyle)), .state(&Self::form_tab_id(form_tab, AuthFormId::S3NewPathStyle)),
Ok(State::One(StateValue::Usize(0))) Ok(State::Single(StateValue::Usize(0)))
) )
} }
@@ -321,7 +323,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::KubeNamespace)) .state(&Self::form_tab_id(form_tab, AuthFormId::KubeNamespace))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -334,7 +336,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::KubeClusterUrl)) .state(&Self::form_tab_id(form_tab, AuthFormId::KubeClusterUrl))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -347,7 +349,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::KubeUsername)) .state(&Self::form_tab_id(form_tab, AuthFormId::KubeUsername))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -360,7 +362,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::KubeClientCert)) .state(&Self::form_tab_id(form_tab, AuthFormId::KubeClientCert))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -373,7 +375,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::KubeClientKey)) .state(&Self::form_tab_id(form_tab, AuthFormId::KubeClientKey))
{ {
Ok(State::One(StateValue::String(x))) if !x.is_empty() => Some(x), Ok(State::Single(StateValue::String(x))) if !x.is_empty() => Some(x),
_ => None, _ => None,
} }
} }
@@ -383,7 +385,7 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::SmbShare)) .state(&Self::form_tab_id(form_tab, AuthFormId::SmbShare))
{ {
Ok(State::One(StateValue::String(x))) => x, Ok(State::Single(StateValue::String(x))) => x,
_ => String::new(), _ => String::new(),
} }
} }
@@ -397,19 +399,19 @@ impl AuthActivity {
.app .app
.state(&Self::form_tab_id(form_tab, AuthFormId::SmbWorkgroup)) .state(&Self::form_tab_id(form_tab, AuthFormId::SmbWorkgroup))
{ {
Ok(State::One(StateValue::String(x))) => Some(x), Ok(State::Single(StateValue::String(x))) => Some(x),
_ => None, _ => None,
} }
} }
pub(in crate::ui::activities::auth) fn get_new_bookmark(&self) -> (String, bool) { pub(in crate::ui::activities::auth) fn get_new_bookmark(&self) -> (String, bool) {
let name = match self.app.state(&Id::BookmarkName) { let name = match self.app.state(&Id::BookmarkName) {
Ok(State::One(StateValue::String(name))) => name, Ok(State::Single(StateValue::String(name))) => name,
_ => String::default(), _ => String::default(),
}; };
if matches!( if matches!(
self.app.state(&Id::BookmarkSavePassword), self.app.state(&Id::BookmarkSavePassword),
Ok(State::One(StateValue::Usize(0))) Ok(State::Single(StateValue::Usize(0)))
) { ) {
(name, true) (name, true)
} else { } else {
+9 -9
View File
@@ -1,4 +1,4 @@
use tuirealm::{Sub, SubClause, SubEventClause}; use tuirealm::subscription::{EventClause, Sub, SubClause};
use super::*; use super::*;
@@ -435,55 +435,55 @@ impl AuthActivity {
Box::<components::GlobalListener>::default(), Box::<components::GlobalListener>::default(),
vec![ vec![
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Esc, code: Key::Esc,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Function(10), code: Key::Function(10),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Char('c'), code: Key::Char('c'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Char('h'), code: Key::Char('h'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Function(1), code: Key::Function(1),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Char('r'), code: Key::Char('r'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Char('s'), code: Key::Char('s'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new(SubEventClause::WindowResize, SubClause::Always), Sub::new(EventClause::WindowResize, SubClause::Always),
], ],
) { ) {
error!("Failed to mount component: {err}"); error!("Failed to mount component: {err}");
+5 -4
View File
@@ -26,7 +26,10 @@ use lib::transfer::{TransferOpts, TransferStates};
use lib::walkdir::WalkdirStates; use lib::walkdir::WalkdirStates;
use session::TransferPayload; use session::TransferPayload;
use tempfile::TempDir; use tempfile::TempDir;
use tuirealm::{Application, EventListenerCfg, NoUserEvent}; use tuirealm::application::Application;
use tuirealm::event::NoUserEvent;
use tuirealm::listener::EventListenerCfg;
use tuirealm::terminal::TerminalAdapter;
use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason}; use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason};
use crate::config::themes::Theme; use crate::config::themes::Theme;
@@ -286,9 +289,7 @@ impl FileTransferActivity {
exit_reason: None, exit_reason: None,
context: None, context: None,
app: Application::init( app: Application::init(
EventListenerCfg::default() EventListenerCfg::default().crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
.poll_timeout(ticks)
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
), ),
redraw: true, redraw: true,
browser: Browser::new(local_pane, remote_pane), browser: Browser::new(local_pane, remote_pane),
+2 -2
View File
@@ -6,7 +6,7 @@ use std::path::{Path, PathBuf};
use remotefs::File; use remotefs::File;
use remotefs::fs::UnixPex; use remotefs::fs::UnixPex;
use tuirealm::{State, StateValue}; use tuirealm::state::{State, StateValue};
use super::browser::FileExplorerTab; use super::browser::FileExplorerTab;
use super::lib::browser::FoundExplorerTab; use super::lib::browser::FoundExplorerTab;
@@ -135,7 +135,7 @@ impl FileTransferActivity {
fn get_selected_index(&self, id: &Id) -> SelectedFileIndex { fn get_selected_index(&self, id: &Id) -> SelectedFileIndex {
match self.app.state(id) { match self.app.state(id) {
Ok(State::One(StateValue::Usize(idx))) => SelectedFileIndex::One(idx), Ok(State::Single(StateValue::Usize(idx))) => SelectedFileIndex::One(idx),
_ => SelectedFileIndex::None, _ => SelectedFileIndex::None,
} }
} }
@@ -9,6 +9,7 @@ use std::time::SystemTime;
use remotefs::File; use remotefs::File;
use remotefs::fs::Metadata; use remotefs::fs::Metadata;
use tuirealm::terminal::TerminalAdapter;
use super::super::ui_result; use super::super::ui_result;
use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload}; use super::{FileTransferActivity, LogLevel, SelectedFile, TransferPayload};
@@ -6,6 +6,8 @@
// ext // ext
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tuirealm::terminal::TerminalAdapter;
use super::{File, FileTransferActivity, LogLevel, TransferPayload}; use super::{File, FileTransferActivity, LogLevel, TransferPayload};
impl FileTransferActivity { impl FileTransferActivity {
@@ -3,7 +3,7 @@
//! this little module exposes the routine to create a pending action on the file transfer activity. //! this little module exposes the routine to create a pending action on the file transfer activity.
//! A pending action is an action which blocks the execution of the application in await of a certain `Msg`. //! A pending action is an action which blocks the execution of the application in await of a certain `Msg`.
use tuirealm::{PollStrategy, Update}; use tuirealm::application::PollStrategy;
use super::{FileTransferActivity, Msg}; use super::{FileTransferActivity, Msg};
@@ -18,7 +18,10 @@ impl FileTransferActivity {
self.redraw = true; self.redraw = true;
loop { loop {
// Poll // Poll
match self.app.tick(PollStrategy::Once) { match self
.app
.tick(PollStrategy::Once(std::time::Duration::from_millis(10)))
{
Ok(mut messages) => { Ok(mut messages) => {
if !messages.is_empty() { if !messages.is_empty() {
self.redraw = true; self.redraw = true;
+6 -6
View File
@@ -2,9 +2,9 @@
//! //!
//! file transfer activity components //! file transfer activity components
use tui_realm_stdlib::Phantom; use tui_realm_stdlib::components::Phantom;
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::{Component, MockComponent, NoUserEvent}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use super::{Msg, TransferMsg, UiMsg}; use super::{Msg, TransferMsg, UiMsg};
@@ -30,13 +30,13 @@ pub use self::log::Log;
pub use self::selected_files::SelectedFilesList; pub use self::selected_files::SelectedFilesList;
pub use self::terminal::Terminal; pub use self::terminal::Terminal;
#[derive(Default, MockComponent)] #[derive(Default, Component)]
pub struct GlobalListener { pub struct GlobalListener {
component: Phantom, component: Phantom,
} }
impl Component<Msg, NoUserEvent> for GlobalListener { impl AppComponent<Msg, NoUserEvent> for GlobalListener {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::ShowDisconnectPopup)) Some(Msg::Ui(UiMsg::ShowDisconnectPopup))
@@ -3,10 +3,14 @@
//! log tab component //! log tab component
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Style, Table}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::props::{
AttrValue, Attribute, Borders, Color, HorizontalAlignment, Props, QueryResult, Style, Table,
Title,
};
use tuirealm::ratatui::widgets::{List as TuiList, ListDirection, ListItem, ListState}; use tuirealm::ratatui::widgets::{List as TuiList, ListDirection, ListItem, ListState};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue}; use tuirealm::state::{State, StateValue};
use super::{Msg, UiMsg}; use super::{Msg, UiMsg};
@@ -31,38 +35,47 @@ impl Log {
} }
} }
impl MockComponent for Log { impl Component for Log {
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::ratatui::layout::Rect) { fn view(
let width: usize = area.width as usize - 4; &mut self,
frame: &mut tuirealm::ratatui::Frame,
area: tuirealm::ratatui::layout::Rect,
) {
let _width: usize = area.width as usize - 4;
let focus = self let focus = self
.props .props
.get_or(Attribute::Focus, AttrValue::Flag(false)) .get(Attribute::Focus)
.unwrap_flag(); .and_then(AttrValue::as_flag)
.unwrap_or(false);
let borders = self let borders = self
.props .props
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default())) .get(Attribute::Borders)
.unwrap_borders(); .and_then(AttrValue::as_borders)
.unwrap_or_default();
let bg = self let bg = self
.props .props
.get_or(Attribute::Background, AttrValue::Color(Color::Reset)) .get(Attribute::Background)
.unwrap_color(); .and_then(AttrValue::as_color)
.unwrap_or(Color::Reset);
// Make list // Make list
let list_items: Vec<ListItem> = self let list_items: Vec<ListItem> = self
.props .props
.get(Attribute::Content) .get(Attribute::Content)
.unwrap() .and_then(AttrValue::as_table)
.unwrap_table() .map(|table| {
.iter() table
.map(|row| { .iter()
let row_refs = row.iter().collect::<Vec<_>>(); .map(|row| {
ListItem::new(tui_realm_stdlib::utils::wrap_spans( let spans = row
row_refs.as_slice(), .iter()
width, .flat_map(|line| line.spans.iter().cloned())
&self.props, .collect::<Vec<_>>();
)) ListItem::new(tuirealm::ratatui::text::Line::from(spans))
})
.collect()
}) })
.collect(); .unwrap_or_default();
let title = ("Log".to_string(), Alignment::Left); let title = Title::from("Log".to_string()).alignment(HorizontalAlignment::Left);
let w = TuiList::new(list_items) let w = TuiList::new(list_items)
.block(tui_realm_stdlib::utils::get_block( .block(tui_realm_stdlib::utils::get_block(
borders, borders,
@@ -79,29 +92,26 @@ impl MockComponent for Log {
frame.render_stateful_widget(w, area, &mut state); frame.render_stateful_widget(w, area, &mut state);
} }
fn query(&self, attr: Attribute) -> Option<AttrValue> { fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
self.props.get(attr) self.props.get_for_query(attr)
} }
fn attr(&mut self, attr: Attribute, value: AttrValue) { fn attr(&mut self, attr: Attribute, value: AttrValue) {
self.props.set(attr, value); self.props.set(attr, value);
if matches!(attr, Attribute::Content) { if matches!(attr, Attribute::Content) {
self.states.set_list_len( let len = self
match self .props
.props .get(Attribute::Content)
.get(Attribute::Content) .and_then(AttrValue::as_table)
.map(AttrValue::unwrap_table) .map(std::vec::Vec::len)
{ .unwrap_or(0);
Some(spans) => spans.len(), self.states.set_list_len(len);
_ => 0,
},
);
self.states.reset_list_index(); self.states.reset_list_index();
} }
} }
fn state(&self) -> State { fn state(&self) -> State {
State::One(StateValue::Usize(self.states.get_list_index())) State::Single(StateValue::Usize(self.states.get_list_index()))
} }
fn perform(&mut self, cmd: Cmd) -> CmdResult { fn perform(&mut self, cmd: Cmd) -> CmdResult {
@@ -112,7 +122,7 @@ impl MockComponent for Log {
if prev != self.states.get_list_index() { if prev != self.states.get_list_index() {
CmdResult::Changed(self.state()) CmdResult::Changed(self.state())
} else { } else {
CmdResult::None CmdResult::NoChange
} }
} }
Cmd::Move(Direction::Up) => { Cmd::Move(Direction::Up) => {
@@ -121,7 +131,7 @@ impl MockComponent for Log {
if prev != self.states.get_list_index() { if prev != self.states.get_list_index() {
CmdResult::Changed(self.state()) CmdResult::Changed(self.state())
} else { } else {
CmdResult::None CmdResult::NoChange
} }
} }
Cmd::Scroll(Direction::Down) => { Cmd::Scroll(Direction::Down) => {
@@ -130,7 +140,7 @@ impl MockComponent for Log {
if prev != self.states.get_list_index() { if prev != self.states.get_list_index() {
CmdResult::Changed(self.state()) CmdResult::Changed(self.state())
} else { } else {
CmdResult::None CmdResult::NoChange
} }
} }
Cmd::Scroll(Direction::Up) => { Cmd::Scroll(Direction::Up) => {
@@ -139,7 +149,7 @@ impl MockComponent for Log {
if prev != self.states.get_list_index() { if prev != self.states.get_list_index() {
CmdResult::Changed(self.state()) CmdResult::Changed(self.state())
} else { } else {
CmdResult::None CmdResult::NoChange
} }
} }
Cmd::GoTo(Position::Begin) => { Cmd::GoTo(Position::Begin) => {
@@ -148,7 +158,7 @@ impl MockComponent for Log {
if prev != self.states.get_list_index() { if prev != self.states.get_list_index() {
CmdResult::Changed(self.state()) CmdResult::Changed(self.state())
} else { } else {
CmdResult::None CmdResult::NoChange
} }
} }
Cmd::GoTo(Position::End) => { Cmd::GoTo(Position::End) => {
@@ -157,16 +167,16 @@ impl MockComponent for Log {
if prev != self.states.get_list_index() { if prev != self.states.get_list_index() {
CmdResult::Changed(self.state()) CmdResult::Changed(self.state())
} else { } else {
CmdResult::None CmdResult::NoChange
} }
} }
_ => CmdResult::None, _ => CmdResult::NoChange,
} }
} }
} }
impl Component<Msg, NoUserEvent> for Log { impl AppComponent<Msg, NoUserEvent> for Log {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Up, .. }) => { Event::Keyboard(KeyEvent { code: Key::Up, .. }) => {
self.perform(Cmd::Move(Direction::Down)); self.perform(Cmd::Move(Direction::Down));
@@ -2,13 +2,15 @@
//! //!
//! file transfer activity components //! file transfer activity components
use tui_realm_stdlib::Span; use tui_realm_stdlib::components::Span;
use tuirealm::props::{Color, TextSpan}; use tuirealm::component::{AppComponent, Component};
use tuirealm::{Component, Event, MockComponent, NoUserEvent}; use tuirealm::event::{Event, NoUserEvent};
use tuirealm::props::{Color, SpanStatic};
use tuirealm::ratatui::style::Stylize;
use super::Msg; use super::Msg;
#[derive(MockComponent)] #[derive(Component)]
pub struct FooterBar { pub struct FooterBar {
component: Span, component: Span,
} }
@@ -17,37 +19,37 @@ impl FooterBar {
pub fn new(key_color: Color) -> Self { pub fn new(key_color: Color) -> Self {
Self { Self {
component: Span::default().spans([ component: Span::default().spans([
TextSpan::from("<F1|H>").bold().fg(key_color), SpanStatic::from("<F1|H>").bold().fg(key_color),
TextSpan::from(" Help "), SpanStatic::from(" Help "),
TextSpan::from("<TAB>").bold().fg(key_color), SpanStatic::from("<TAB>").bold().fg(key_color),
TextSpan::from(" Change tab "), SpanStatic::from(" Change tab "),
TextSpan::from("<SPACE>").bold().fg(key_color), SpanStatic::from("<SPACE>").bold().fg(key_color),
TextSpan::from(" Transfer "), SpanStatic::from(" Transfer "),
TextSpan::from("<ENTER>").bold().fg(key_color), SpanStatic::from("<ENTER>").bold().fg(key_color),
TextSpan::from(" Enter dir "), SpanStatic::from(" Enter dir "),
TextSpan::from("<F2|S>").bold().fg(key_color), SpanStatic::from("<F2|S>").bold().fg(key_color),
TextSpan::from(" Save as "), SpanStatic::from(" Save as "),
TextSpan::from("<F3|V>").bold().fg(key_color), SpanStatic::from("<F3|V>").bold().fg(key_color),
TextSpan::from(" View "), SpanStatic::from(" View "),
TextSpan::from("<F4|O>").bold().fg(key_color), SpanStatic::from("<F4|O>").bold().fg(key_color),
TextSpan::from(" Edit "), SpanStatic::from(" Edit "),
TextSpan::from("<F5|C>").bold().fg(key_color), SpanStatic::from("<F5|C>").bold().fg(key_color),
TextSpan::from(" Copy "), SpanStatic::from(" Copy "),
TextSpan::from("<F6|R>").bold().fg(key_color), SpanStatic::from("<F6|R>").bold().fg(key_color),
TextSpan::from(" Rename "), SpanStatic::from(" Rename "),
TextSpan::from("<F7|D>").bold().fg(key_color), SpanStatic::from("<F7|D>").bold().fg(key_color),
TextSpan::from(" Make dir "), SpanStatic::from(" Make dir "),
TextSpan::from("<F8|DEL>").bold().fg(key_color), SpanStatic::from("<F8|DEL>").bold().fg(key_color),
TextSpan::from(" Delete "), SpanStatic::from(" Delete "),
TextSpan::from("<F10|Q>").bold().fg(key_color), SpanStatic::from("<F10|Q>").bold().fg(key_color),
TextSpan::from(" Quit "), SpanStatic::from(" Quit "),
]), ]),
} }
} }
} }
impl Component<Msg, NoUserEvent> for FooterBar { impl AppComponent<Msg, NoUserEvent> for FooterBar {
fn on(&mut self, _: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
@@ -1,10 +1,13 @@
use remotefs::fs::{UnixPex, UnixPexClass}; use remotefs::fs::{UnixPex, UnixPexClass};
use tui_realm_stdlib::Checkbox; use tui_realm_stdlib::components::Checkbox;
use tuirealm::command::{Cmd, CmdResult, Direction}; use tuirealm::command::{Cmd, CmdResult, Direction};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, AttrValue, Attribute, BorderSides, Borders, Color}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::props::{
AttrValue, Attribute, BorderSides, Borders, Color, HorizontalAlignment, Props, Title,
};
use tuirealm::ratatui::layout::{Constraint, Direction as LayoutDirection, Layout}; use tuirealm::ratatui::layout::{Constraint, Direction as LayoutDirection, Layout};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, Props, State, StateValue}; use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
@@ -60,21 +63,21 @@ impl ChmodPopup {
user: Checkbox::default() user: Checkbox::default()
.foreground(color) .foreground(color)
.choices(["Read", "Write", "Execute"]) .choices(["Read", "Write", "Execute"])
.title("User", Alignment::Left) .title(Title::from("User").alignment(HorizontalAlignment::Left))
.borders(Borders::default().sides(BorderSides::NONE)) .borders(Borders::default().sides(BorderSides::NONE))
.values(&make_pex_values(pex.user())) .values(&make_pex_values(pex.user()))
.rewind(true), .rewind(true),
group: Checkbox::default() group: Checkbox::default()
.foreground(color) .foreground(color)
.choices(["Read", "Write", "Execute"]) .choices(["Read", "Write", "Execute"])
.title("Group", Alignment::Left) .title(Title::from("Group").alignment(HorizontalAlignment::Left))
.borders(Borders::default().sides(BorderSides::NONE)) .borders(Borders::default().sides(BorderSides::NONE))
.values(&make_pex_values(pex.group())) .values(&make_pex_values(pex.group()))
.rewind(true), .rewind(true),
others: Checkbox::default() others: Checkbox::default()
.foreground(color) .foreground(color)
.choices(["Read", "Write", "Execute"]) .choices(["Read", "Write", "Execute"])
.title("Others", Alignment::Left) .title(Title::from("Others").alignment(HorizontalAlignment::Left))
.borders(Borders::default().sides(BorderSides::NONE)) .borders(Borders::default().sides(BorderSides::NONE))
.values(&make_pex_values(pex.others())) .values(&make_pex_values(pex.others()))
.rewind(true), .rewind(true),
@@ -146,8 +149,8 @@ impl ChmodPopup {
} }
} }
impl MockComponent for ChmodPopup { impl Component for ChmodPopup {
fn attr(&mut self, attr: tuirealm::Attribute, value: AttrValue) { fn attr(&mut self, attr: tuirealm::props::Attribute, value: AttrValue) {
self.props.set(attr, value.clone()); self.props.set(attr, value.clone());
if attr == Attribute::Focus { if attr == Attribute::Focus {
@@ -166,28 +169,40 @@ impl MockComponent for ChmodPopup {
} }
Cmd::Move(Direction::Up) => { Cmd::Move(Direction::Up) => {
self.active_checkbox_up(); self.active_checkbox_up();
CmdResult::None CmdResult::NoChange
} }
Cmd::Move(Direction::Down) => { Cmd::Move(Direction::Down) => {
self.active_checkbox_down(); self.active_checkbox_down();
CmdResult::None CmdResult::NoChange
} }
Cmd::Toggle => self.get_active_checkbox().perform(cmd), Cmd::Toggle => self.get_active_checkbox().perform(cmd),
Cmd::Submit => CmdResult::Submit(self.state()), Cmd::Submit => CmdResult::Submit(self.state()),
_ => CmdResult::None, _ => CmdResult::NoChange,
} }
} }
fn query(&self, attr: tuirealm::Attribute) -> Option<AttrValue> { fn query<'a>(
self.props.get(attr) &'a self,
attr: tuirealm::props::Attribute,
) -> Option<tuirealm::props::QueryResult<'a>> {
self.props.get_for_query(attr)
} }
fn state(&self) -> State { fn state(&self) -> State {
State::One(StateValue::U32(self.get_mode().into())) State::Single(StateValue::U32(self.get_mode().into()))
} }
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::ratatui::layout::Rect) { fn view(
if self.props.get_or(Attribute::Display, AttrValue::Flag(true)) != AttrValue::Flag(true) { &mut self,
frame: &mut tuirealm::ratatui::Frame,
area: tuirealm::ratatui::layout::Rect,
) {
if !self
.props
.get(Attribute::Display)
.and_then(AttrValue::as_flag)
.unwrap_or(true)
{
return; return;
} }
let chunks = Layout::default() let chunks = Layout::default()
@@ -205,10 +220,11 @@ impl MockComponent for ChmodPopup {
let focus = self let focus = self
.props .props
.get_or(Attribute::Focus, AttrValue::Flag(false)) .get(Attribute::Focus)
.unwrap_flag(); .and_then(AttrValue::as_flag)
.unwrap_or(false);
let div_title = (self.title.clone(), Alignment::Center); let div_title = Title::from(self.title.clone()).alignment(HorizontalAlignment::Center);
let div = tui_realm_stdlib::utils::get_block( let div = tui_realm_stdlib::utils::get_block(
Borders::default().color(self.color), Borders::default().color(self.color),
@@ -225,8 +241,8 @@ impl MockComponent for ChmodPopup {
} }
} }
impl Component<Msg, NoUserEvent> for ChmodPopup { impl AppComponent<Msg, NoUserEvent> for ChmodPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseChmodPopup)) Some(Msg::Ui(UiMsg::CloseChmodPopup))
@@ -1,12 +1,13 @@
use tui_realm_stdlib::Input; use tui_realm_stdlib::components::Input;
use tuirealm::command::{Cmd, Direction, Position}; use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{BorderType, Borders, Color, HorizontalAlignment, InputType, Style, Title};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct CopyPopup { pub struct CopyPopup {
component: Input, component: Input,
} }
@@ -22,17 +23,19 @@ impl CopyPopup {
) )
.foreground(color) .foreground(color)
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"destination", "destination",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
) ))
.title("Copy file(s) to\u{2026}", Alignment::Center), .title(
Title::from("Copy file(s) to\u{2026}").alignment(HorizontalAlignment::Center),
),
} }
} }
} }
impl Component<Msg, NoUserEvent> for CopyPopup { impl AppComponent<Msg, NoUserEvent> for CopyPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -73,13 +76,13 @@ impl Component<Msg, NoUserEvent> for CopyPopup {
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
self.perform(Cmd::Type(ch)); self.perform(Cmd::Type(*ch));
Some(Msg::None) Some(Msg::None)
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.state() { }) => match self.state() {
State::One(StateValue::String(i)) => { State::Single(StateValue::String(i)) => {
Some(Msg::Transfer(TransferMsg::CopyFileTo(i))) Some(Msg::Transfer(TransferMsg::CopyFileTo(i)))
} }
_ => Some(Msg::None), _ => Some(Msg::None),
@@ -1,12 +1,15 @@
use tui_realm_stdlib::Radio; use tui_realm_stdlib::components::Radio;
use tuirealm::command::{Cmd, CmdResult, Direction}; use tuirealm::command::{Cmd, CmdResult, Direction};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{
BorderType, Borders, Color, HorizontalAlignment, Style, TextModifiers, Title,
};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct DeletePopup { pub struct DeletePopup {
component: Radio, component: Radio,
} }
@@ -15,21 +18,25 @@ impl DeletePopup {
pub fn new(color: Color) -> Self { pub fn new(color: Color) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color)
.choices(["Yes", "No"]) .choices(["Yes", "No"])
.value(1) .value(1)
.title("Delete file(s)?", Alignment::Center), .title(Title::from("Delete file(s)?").alignment(HorizontalAlignment::Center)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for DeletePopup { impl AppComponent<Msg, NoUserEvent> for DeletePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -59,7 +66,7 @@ impl Component<Msg, NoUserEvent> for DeletePopup {
}) => { }) => {
if matches!( if matches!(
self.perform(Cmd::Submit), self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0))) CmdResult::Submit(State::Single(StateValue::Usize(0)))
) { ) {
Some(Msg::Transfer(TransferMsg::DeleteFile)) Some(Msg::Transfer(TransferMsg::DeleteFile))
} else { } else {
@@ -1,12 +1,15 @@
use tui_realm_stdlib::Radio; use tui_realm_stdlib::components::Radio;
use tuirealm::command::{Cmd, CmdResult, Direction}; use tuirealm::command::{Cmd, CmdResult, Direction};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{
BorderType, Borders, Color, HorizontalAlignment, Style, TextModifiers, Title,
};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct DisconnectPopup { pub struct DisconnectPopup {
component: Radio, component: Radio,
} }
@@ -15,20 +18,27 @@ impl DisconnectPopup {
pub fn new(color: Color) -> Self { pub fn new(color: Color) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color)
.choices(["Yes", "No"]) .choices(["Yes", "No"])
.title("Are you sure you want to disconnect?", Alignment::Center), .title(
Title::from("Are you sure you want to disconnect?")
.alignment(HorizontalAlignment::Center),
),
} }
} }
} }
impl Component<Msg, NoUserEvent> for DisconnectPopup { impl AppComponent<Msg, NoUserEvent> for DisconnectPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -58,7 +68,7 @@ impl Component<Msg, NoUserEvent> for DisconnectPopup {
}) => { }) => {
if matches!( if matches!(
self.perform(Cmd::Submit), self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0))) CmdResult::Submit(State::Single(StateValue::Usize(0)))
) { ) {
Some(Msg::Ui(UiMsg::Disconnect)) Some(Msg::Ui(UiMsg::Disconnect))
} else { } else {
@@ -1,11 +1,12 @@
use tui_realm_stdlib::Paragraph; use tui_realm_stdlib::components::Paragraph;
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent}; use tuirealm::props::{BorderType, Borders, Color, HorizontalAlignment, SpanStatic};
use tuirealm::ratatui::text::Text;
use crate::ui::activities::filetransfer::{Msg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct ErrorPopup { pub struct ErrorPopup {
component: Paragraph, component: Paragraph,
} }
@@ -14,21 +15,23 @@ impl ErrorPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self { pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self { Self {
component: Paragraph::default() component: Paragraph::default()
.alignment(Alignment::Center) .alignment_horizontal(HorizontalAlignment::Center)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.text([TextSpan::from(text.as_ref())]) .text(Text::from_iter([SpanStatic::from(
.wrap(true), text.as_ref().to_string(),
)]))
.wrap_trim(true),
} }
} }
} }
impl Component<Msg, NoUserEvent> for ErrorPopup { impl AppComponent<Msg, NoUserEvent> for ErrorPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter, code: Key::Esc | Key::Enter,
@@ -39,7 +42,7 @@ impl Component<Msg, NoUserEvent> for ErrorPopup {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct FatalPopup { pub struct FatalPopup {
component: Paragraph, component: Paragraph,
} }
@@ -48,21 +51,23 @@ impl FatalPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self { pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self { Self {
component: Paragraph::default() component: Paragraph::default()
.alignment(Alignment::Center) .alignment_horizontal(HorizontalAlignment::Center)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.text([TextSpan::from(text.as_ref())]) .text(Text::from_iter([SpanStatic::from(
.wrap(true), text.as_ref().to_string(),
)]))
.wrap_trim(true),
} }
} }
} }
impl Component<Msg, NoUserEvent> for FatalPopup { impl AppComponent<Msg, NoUserEvent> for FatalPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter, code: Key::Esc | Key::Enter,
@@ -2,17 +2,20 @@ use std::time::UNIX_EPOCH;
use bytesize::ByteSize; use bytesize::ByteSize;
use remotefs::File; use remotefs::File;
use tui_realm_stdlib::List; use tui_realm_stdlib::components::List;
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, TableBuilder, TextSpan}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent}; use tuirealm::props::{
BorderType, Borders, Color, HorizontalAlignment, SpanStatic, TableBuilder, Title,
};
use tuirealm::ratatui::style::Stylize;
#[cfg(posix)] #[cfg(posix)]
use uzers::{get_group_by_gid, get_user_by_uid}; use uzers::{get_group_by_gid, get_user_by_uid};
use crate::ui::activities::filetransfer::{Msg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, UiMsg};
use crate::utils::fmt::fmt_time; use crate::utils::fmt::fmt_time;
#[derive(MockComponent)] #[derive(Component)]
pub struct FileInfoPopup { pub struct FileInfoPopup {
component: List, component: List,
} }
@@ -28,23 +31,23 @@ impl FileInfoPopup {
}; };
// Make texts // Make texts
texts texts
.add_col(TextSpan::from("Path: ")) .add_col(SpanStatic::from("Path: "))
.add_col(TextSpan::new(path.as_str()).fg(Color::Yellow)); .add_col(SpanStatic::raw(path.clone()).fg(Color::Yellow));
texts texts
.add_row() .add_row()
.add_col(TextSpan::from("Name: ")) .add_col(SpanStatic::from("Name: "))
.add_col(TextSpan::new(file.name()).fg(Color::Yellow)); .add_col(SpanStatic::raw(file.name().clone()).fg(Color::Yellow));
if let Some(filetype) = file.extension() { if let Some(filetype) = file.extension() {
texts texts
.add_row() .add_row()
.add_col(TextSpan::from("File type: ")) .add_col(SpanStatic::from("File type: "))
.add_col(TextSpan::new(filetype).fg(Color::LightGreen)); .add_col(SpanStatic::raw(filetype.clone()).fg(Color::LightGreen));
} }
let (bsize, size): (ByteSize, u64) = (ByteSize(file.metadata().size), file.metadata().size); let (bsize, size): (ByteSize, u64) = (ByteSize(file.metadata().size), file.metadata().size);
texts texts
.add_row() .add_row()
.add_col(TextSpan::from("Size: ")) .add_col(SpanStatic::from("Size: "))
.add_col(TextSpan::new(format!("{bsize} ({size})").as_str()).fg(Color::Cyan)); .add_col(SpanStatic::raw(format!("{bsize} ({size})")).fg(Color::Cyan));
let atime: String = fmt_time( let atime: String = fmt_time(
file.metadata().accessed.unwrap_or(UNIX_EPOCH), file.metadata().accessed.unwrap_or(UNIX_EPOCH),
"%b %d %Y %H:%M:%S", "%b %d %Y %H:%M:%S",
@@ -59,16 +62,16 @@ impl FileInfoPopup {
); );
texts texts
.add_row() .add_row()
.add_col(TextSpan::from("Creation time: ")) .add_col(SpanStatic::from("Creation time: "))
.add_col(TextSpan::new(ctime.as_str()).fg(Color::LightGreen)); .add_col(SpanStatic::raw(ctime.clone()).fg(Color::LightGreen));
texts texts
.add_row() .add_row()
.add_col(TextSpan::from("Last modified time: ")) .add_col(SpanStatic::from("Last modified time: "))
.add_col(TextSpan::new(mtime.as_str()).fg(Color::LightBlue)); .add_col(SpanStatic::raw(mtime.clone()).fg(Color::LightBlue));
texts texts
.add_row() .add_row()
.add_col(TextSpan::from("Last access time: ")) .add_col(SpanStatic::from("Last access time: "))
.add_col(TextSpan::new(atime.as_str()).fg(Color::LightRed)); .add_col(SpanStatic::raw(atime.clone()).fg(Color::LightRed));
// User // User
#[cfg(posix)] #[cfg(posix)]
let username: String = match file.metadata().uid { let username: String = match file.metadata().uid {
@@ -93,24 +96,29 @@ impl FileInfoPopup {
let group: String = format!("{}", file.metadata().gid.unwrap_or(0)); let group: String = format!("{}", file.metadata().gid.unwrap_or(0));
texts texts
.add_row() .add_row()
.add_col(TextSpan::from("User: ")) .add_col(SpanStatic::from("User: "))
.add_col(TextSpan::new(username.as_str()).fg(Color::LightYellow)); .add_col(SpanStatic::raw(username.clone()).fg(Color::LightYellow));
texts texts
.add_row() .add_row()
.add_col(TextSpan::from("Group: ")) .add_col(SpanStatic::from("Group: "))
.add_col(TextSpan::new(group.as_str()).fg(Color::Blue)); .add_col(SpanStatic::raw(group.clone()).fg(Color::Blue));
Self { Self {
component: List::default() component: List::default()
.borders(Borders::default().modifiers(BorderType::Rounded)) .borders(Borders::default().modifiers(BorderType::Rounded))
.scroll(false) .scroll(false)
.title(file.name(), Alignment::Left) .title(Title::from(file.name().clone()).alignment(HorizontalAlignment::Left))
.rows(texts.build()), .rows(
texts
.build()
.into_iter()
.map(|row| row.into_iter().flat_map(|l| l.spans).collect::<Vec<_>>()),
),
} }
} }
} }
impl Component<Msg, NoUserEvent> for FileInfoPopup { impl AppComponent<Msg, NoUserEvent> for FileInfoPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter, code: Key::Esc | Key::Enter,
@@ -1,12 +1,13 @@
use tui_realm_stdlib::Input; use tui_realm_stdlib::components::Input;
use tuirealm::command::{Cmd, Direction, Position}; use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{BorderType, Borders, Color, HorizontalAlignment, InputType, Style, Title};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct FilterPopup { pub struct FilterPopup {
component: Input, component: Input,
} }
@@ -22,20 +23,20 @@ impl FilterPopup {
) )
.foreground(color) .foreground(color)
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"regex or wildmatch", "regex or wildmatch",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
) ))
.title( .title(
"Filter files by regex or wildmatch in the current directory", Title::from("Filter files by regex or wildmatch in the current directory")
Alignment::Center, .alignment(HorizontalAlignment::Center),
), ),
} }
} }
} }
impl Component<Msg, NoUserEvent> for FilterPopup { impl AppComponent<Msg, NoUserEvent> for FilterPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -76,13 +77,15 @@ impl Component<Msg, NoUserEvent> for FilterPopup {
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
self.perform(Cmd::Type(ch)); self.perform(Cmd::Type(*ch));
Some(Msg::None) Some(Msg::None)
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.state() { }) => match self.state() {
State::One(StateValue::String(filter)) => Some(Msg::Ui(UiMsg::FilterFiles(filter))), State::Single(StateValue::String(filter)) => {
Some(Msg::Ui(UiMsg::FilterFiles(filter)))
}
_ => Some(Msg::None), _ => Some(Msg::None),
}, },
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
@@ -1,12 +1,14 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tui_realm_stdlib::Input; use tui_realm_stdlib::components::Input;
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, PropValue, Style}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{ use tuirealm::props::{
AttrValue, Attribute, Component, Event, MockComponent, NoUserEvent, State, StateValue, AttrValue, Attribute, BorderType, Borders, Color, HorizontalAlignment, InputType, PropValue,
Style, Title,
}; };
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
@@ -51,14 +53,15 @@ impl From<CmdResult> for Suggestion {
fn from(value: CmdResult) -> Self { fn from(value: CmdResult) -> Self {
match value { match value {
CmdResult::Batch(v) if v.len() == 1 => { CmdResult::Batch(v) if v.len() == 1 => {
if let CmdResult::Submit(State::One(StateValue::String(s))) = v.first().unwrap() { if let CmdResult::Submit(State::Single(StateValue::String(s))) = v.first().unwrap()
{
Suggestion::Suggest(s.clone()) Suggestion::Suggest(s.clone())
} else { } else {
Suggestion::None Suggestion::None
} }
} }
CmdResult::Batch(v) if v.len() == 2 => { CmdResult::Batch(v) if v.len() == 2 => {
if let CmdResult::Submit(State::One(StateValue::String(s))) = v.get(1).unwrap() { if let CmdResult::Submit(State::Single(StateValue::String(s))) = v.get(1).unwrap() {
Suggestion::Rescan(PathBuf::from(s)) Suggestion::Rescan(PathBuf::from(s))
} else { } else {
Suggestion::None Suggestion::None
@@ -72,13 +75,13 @@ impl From<CmdResult> for Suggestion {
impl From<Suggestion> for CmdResult { impl From<Suggestion> for CmdResult {
fn from(value: Suggestion) -> Self { fn from(value: Suggestion) -> Self {
match value { match value {
Suggestion::None => CmdResult::None, Suggestion::None => CmdResult::NoChange,
Suggestion::Suggest(s) => { Suggestion::Suggest(s) => CmdResult::Batch(vec![CmdResult::Submit(State::Single(
CmdResult::Batch(vec![CmdResult::Submit(State::One(StateValue::String(s)))]) StateValue::String(s),
} ))]),
Suggestion::Rescan(p) => CmdResult::Batch(vec![ Suggestion::Rescan(p) => CmdResult::Batch(vec![
CmdResult::None, CmdResult::NoChange,
CmdResult::Submit(State::One(StateValue::String( CmdResult::Submit(State::Single(StateValue::String(
p.to_string_lossy().to_string(), p.to_string_lossy().to_string(),
))), ))),
]), ]),
@@ -187,18 +190,25 @@ impl GotoPopup {
) )
.foreground(color) .foreground(color)
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"/foo/bar/buzz", "/foo/bar/buzz",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
) ))
.title("Go to… (Press <TAB> for autocompletion)", Alignment::Center), .title(
Title::from("Go to… (Press <TAB> for autocompletion)")
.alignment(HorizontalAlignment::Center),
),
states, states,
} }
} }
} }
impl MockComponent for GotoPopup { impl Component for GotoPopup {
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::ratatui::prelude::Rect) { fn view(
&mut self,
frame: &mut tuirealm::ratatui::Frame,
area: tuirealm::ratatui::prelude::Rect,
) {
self.input.view(frame, area); self.input.view(frame, area);
} }
@@ -220,12 +230,12 @@ impl MockComponent for GotoPopup {
} }
} }
fn query(&self, attr: Attribute) -> Option<AttrValue> { fn query<'a>(&'a self, attr: Attribute) -> Option<tuirealm::props::QueryResult<'a>> {
self.input.query(attr) self.input.query(attr)
} }
fn state(&self) -> State { fn state(&self) -> State {
State::One(StateValue::String(self.states.computed_search())) State::Single(StateValue::String(self.states.computed_search()))
} }
fn perform(&mut self, cmd: Cmd) -> CmdResult { fn perform(&mut self, cmd: Cmd) -> CmdResult {
@@ -236,7 +246,7 @@ impl MockComponent for GotoPopup {
.search .search
.as_ref() .as_ref()
.cloned() .cloned()
.unwrap_or_else(|| self.input.state().unwrap_one().unwrap_string()); .unwrap_or_else(|| self.input.state().unwrap_single().unwrap_string());
let suggest = self.states.suggest(&input); let suggest = self.states.suggest(&input);
if let Suggestion::Suggest(suggestion) = suggest.clone() { if let Suggestion::Suggest(suggestion) = suggest.clone() {
self.input self.input
@@ -247,7 +257,7 @@ impl MockComponent for GotoPopup {
} }
cmd => { cmd => {
let res = self.input.perform(cmd); let res = self.input.perform(cmd);
if let CmdResult::Changed(State::One(StateValue::String(new_text))) = &res { if let CmdResult::Changed(State::Single(StateValue::String(new_text))) = &res {
self.states.search = Some(new_text.clone()); self.states.search = Some(new_text.clone());
} }
res res
@@ -256,8 +266,8 @@ impl MockComponent for GotoPopup {
} }
} }
impl Component<Msg, NoUserEvent> for GotoPopup { impl AppComponent<Msg, NoUserEvent> for GotoPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -298,7 +308,7 @@ impl Component<Msg, NoUserEvent> for GotoPopup {
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
self.perform(Cmd::Type(ch)); self.perform(Cmd::Type(*ch));
Some(Msg::None) Some(Msg::None)
} }
Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => { Event::Keyboard(KeyEvent { code: Key::Tab, .. }) => {
@@ -311,7 +321,7 @@ impl Component<Msg, NoUserEvent> for GotoPopup {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.state() { }) => match self.state() {
State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::GoTo(i))), State::Single(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::GoTo(i))),
_ => Some(Msg::None), _ => Some(Msg::None),
}, },
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
@@ -1,12 +1,15 @@
use tui_realm_stdlib::List; use tui_realm_stdlib::components::List;
use tuirealm::command::{Cmd, Direction, Position}; use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, TableBuilder, TextSpan}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent}; use tuirealm::props::{
BorderType, Borders, Color, HorizontalAlignment, SpanStatic, TableBuilder, Title,
};
use tuirealm::ratatui::style::Stylize;
use crate::ui::activities::filetransfer::{Msg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct KeybindingsPopup { pub struct KeybindingsPopup {
component: List, component: List,
} }
@@ -18,148 +21,152 @@ impl KeybindingsPopup {
.borders(Borders::default().modifiers(BorderType::Rounded)) .borders(Borders::default().modifiers(BorderType::Rounded))
.scroll(true) .scroll(true)
.step(8) .step(8)
.highlighted_str("? ") .highlight_str("? ")
.title("Keybindings", Alignment::Center) .title(Title::from("Keybindings").alignment(HorizontalAlignment::Center))
.rewind(true) .rewind(true)
.rows( .rows(
TableBuilder::default() TableBuilder::default()
.add_col(TextSpan::new("<ESC>").bold().fg(key_color)) .add_col(SpanStatic::raw("<ESC>").bold().fg(key_color))
.add_col(TextSpan::from(" Disconnect")) .add_col(SpanStatic::from(" Disconnect"))
.add_row() .add_row()
.add_col(TextSpan::new("<BACKSPACE>").bold().fg(key_color)) .add_col(SpanStatic::raw("<BACKSPACE>").bold().fg(key_color))
.add_col(TextSpan::from(" Go to previous directory")) .add_col(SpanStatic::from(" Go to previous directory"))
.add_row() .add_row()
.add_col(TextSpan::new("<TAB|RIGHT|LEFT>").bold().fg(key_color)) .add_col(SpanStatic::raw("<TAB|RIGHT|LEFT>").bold().fg(key_color))
.add_col(TextSpan::from(" Change explorer tab")) .add_col(SpanStatic::from(" Change explorer tab"))
.add_row() .add_row()
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(key_color)) .add_col(SpanStatic::raw("<UP/DOWN>").bold().fg(key_color))
.add_col(TextSpan::from(" Move up/down in list")) .add_col(SpanStatic::from(" Move up/down in list"))
.add_row() .add_row()
.add_col(TextSpan::new("<ENTER>").bold().fg(key_color)) .add_col(SpanStatic::raw("<ENTER>").bold().fg(key_color))
.add_col(TextSpan::from(" Enter directory")) .add_col(SpanStatic::from(" Enter directory"))
.add_row() .add_row()
.add_col(TextSpan::new("<SPACE>").bold().fg(key_color)) .add_col(SpanStatic::raw("<SPACE>").bold().fg(key_color))
.add_col(TextSpan::from(" Upload/Download file")) .add_col(SpanStatic::from(" Upload/Download file"))
.add_row() .add_row()
.add_col(TextSpan::new("<BACKTAB>").bold().fg(key_color)) .add_col(SpanStatic::raw("<BACKTAB>").bold().fg(key_color))
.add_col(TextSpan::from( .add_col(SpanStatic::from(
" Switch between explorer and log window", " Switch between explorer and log window",
)) ))
.add_row() .add_row()
.add_col(TextSpan::new("<A>").bold().fg(key_color)) .add_col(SpanStatic::raw("<A>").bold().fg(key_color))
.add_col(TextSpan::from(" Toggle hidden files")) .add_col(SpanStatic::from(" Toggle hidden files"))
.add_row() .add_row()
.add_col(TextSpan::new("<B>").bold().fg(key_color)) .add_col(SpanStatic::raw("<B>").bold().fg(key_color))
.add_col(TextSpan::from(" Change file sorting mode")) .add_col(SpanStatic::from(" Change file sorting mode"))
.add_row() .add_row()
.add_col(TextSpan::new("<C|F5>").bold().fg(key_color)) .add_col(SpanStatic::raw("<C|F5>").bold().fg(key_color))
.add_col(TextSpan::from(" Copy")) .add_col(SpanStatic::from(" Copy"))
.add_row() .add_row()
.add_col(TextSpan::new("<D|F7>").bold().fg(key_color)) .add_col(SpanStatic::raw("<D|F7>").bold().fg(key_color))
.add_col(TextSpan::from(" Make directory")) .add_col(SpanStatic::from(" Make directory"))
.add_row() .add_row()
.add_col(TextSpan::new("<F>").bold().fg(key_color)) .add_col(SpanStatic::raw("<F>").bold().fg(key_color))
.add_col(TextSpan::from(" Search files")) .add_col(SpanStatic::from(" Search files"))
.add_row() .add_row()
.add_col(TextSpan::new("<G>").bold().fg(key_color)) .add_col(SpanStatic::raw("<G>").bold().fg(key_color))
.add_col(TextSpan::from(" Go to path")) .add_col(SpanStatic::from(" Go to path"))
.add_row() .add_row()
.add_col(TextSpan::new("<H|F1>").bold().fg(key_color)) .add_col(SpanStatic::raw("<H|F1>").bold().fg(key_color))
.add_col(TextSpan::from(" Show help")) .add_col(SpanStatic::from(" Show help"))
.add_row() .add_row()
.add_col(TextSpan::new("<I>").bold().fg(key_color)) .add_col(SpanStatic::raw("<I>").bold().fg(key_color))
.add_col(TextSpan::from( .add_col(SpanStatic::from(
" Show info about selected file", " Show info about selected file",
)) ))
.add_row() .add_row()
.add_col(TextSpan::new("<K>").bold().fg(key_color)) .add_col(SpanStatic::raw("<K>").bold().fg(key_color))
.add_col(TextSpan::from( .add_col(SpanStatic::from(
" Create symlink pointing to the current selected entry", " Create symlink pointing to the current selected entry",
)) ))
.add_row() .add_row()
.add_col(TextSpan::new("<L>").bold().fg(key_color)) .add_col(SpanStatic::raw("<L>").bold().fg(key_color))
.add_col(TextSpan::from(" Reload directory content")) .add_col(SpanStatic::from(" Reload directory content"))
.add_row() .add_row()
.add_col(TextSpan::new("<M>").bold().fg(key_color)) .add_col(SpanStatic::raw("<M>").bold().fg(key_color))
.add_col(TextSpan::from(" Select file")) .add_col(SpanStatic::from(" Select file"))
.add_row() .add_row()
.add_col(TextSpan::new("<N>").bold().fg(key_color)) .add_col(SpanStatic::raw("<N>").bold().fg(key_color))
.add_col(TextSpan::from(" Create new file")) .add_col(SpanStatic::from(" Create new file"))
.add_row() .add_row()
.add_col(TextSpan::new("<O|F4>").bold().fg(key_color)) .add_col(SpanStatic::raw("<O|F4>").bold().fg(key_color))
.add_col(TextSpan::from( .add_col(SpanStatic::from(
" Open text file with preferred editor", " Open text file with preferred editor",
)) ))
.add_row() .add_row()
.add_col(TextSpan::new("<P>").bold().fg(key_color)) .add_col(SpanStatic::raw("<P>").bold().fg(key_color))
.add_col(TextSpan::from(" Toggle bottom panel")) .add_col(SpanStatic::from(" Toggle bottom panel"))
.add_row() .add_row()
.add_col(TextSpan::new("<Q|F10>").bold().fg(key_color)) .add_col(SpanStatic::raw("<Q|F10>").bold().fg(key_color))
.add_col(TextSpan::from(" Quit termscp")) .add_col(SpanStatic::from(" Quit termscp"))
.add_row() .add_row()
.add_col(TextSpan::new("<R|F6>").bold().fg(key_color)) .add_col(SpanStatic::raw("<R|F6>").bold().fg(key_color))
.add_col(TextSpan::from(" Rename file")) .add_col(SpanStatic::from(" Rename file"))
.add_row() .add_row()
.add_col(TextSpan::new("<S|F2>").bold().fg(key_color)) .add_col(SpanStatic::raw("<S|F2>").bold().fg(key_color))
.add_col(TextSpan::from(" Save file as")) .add_col(SpanStatic::from(" Save file as"))
.add_row() .add_row()
.add_col(TextSpan::new("<T>").bold().fg(key_color)) .add_col(SpanStatic::raw("<T>").bold().fg(key_color))
.add_col(TextSpan::from(" Watch/unwatch file changes")) .add_col(SpanStatic::from(
" Watch/unwatch file changes",
))
.add_row() .add_row()
.add_col(TextSpan::new("<U>").bold().fg(key_color)) .add_col(SpanStatic::raw("<U>").bold().fg(key_color))
.add_col(TextSpan::from(" Go to parent directory")) .add_col(SpanStatic::from(" Go to parent directory"))
.add_row() .add_row()
.add_col(TextSpan::new("<V|F3>").bold().fg(key_color)) .add_col(SpanStatic::raw("<V|F3>").bold().fg(key_color))
.add_col(TextSpan::from( .add_col(SpanStatic::from(
" Open file with default application for file type", " Open file with default application for file type",
)) ))
.add_row() .add_row()
.add_col(TextSpan::new("<W>").bold().fg(key_color)) .add_col(SpanStatic::raw("<W>").bold().fg(key_color))
.add_col(TextSpan::from( .add_col(SpanStatic::from(
" Open file with specified application", " Open file with specified application",
)) ))
.add_row() .add_row()
.add_col(TextSpan::new("<X>").bold().fg(key_color)) .add_col(SpanStatic::raw("<X>").bold().fg(key_color))
.add_col(TextSpan::from(" Execute shell command")) .add_col(SpanStatic::from(" Execute shell command"))
.add_row() .add_row()
.add_col(TextSpan::new("<Y>").bold().fg(key_color)) .add_col(SpanStatic::raw("<Y>").bold().fg(key_color))
.add_col(TextSpan::from( .add_col(SpanStatic::from(
" Toggle synchronized browsing", " Toggle synchronized browsing",
)) ))
.add_row() .add_row()
.add_col(TextSpan::new("<Z>").bold().fg(key_color)) .add_col(SpanStatic::raw("<Z>").bold().fg(key_color))
.add_col(TextSpan::from(" Change file permissions")) .add_col(SpanStatic::from(" Change file permissions"))
.add_row() .add_row()
.add_col(TextSpan::new("</>").bold().fg(key_color)) .add_col(SpanStatic::raw("</>").bold().fg(key_color))
.add_col(TextSpan::from(" Filter files")) .add_col(SpanStatic::from(" Filter files"))
.add_row() .add_row()
.add_col(TextSpan::new("<DEL|F8|E>").bold().fg(key_color)) .add_col(SpanStatic::raw("<DEL|F8|E>").bold().fg(key_color))
.add_col(TextSpan::from(" Delete selected file")) .add_col(SpanStatic::from(" Delete selected file"))
.add_row() .add_row()
.add_col(TextSpan::new("<CTRL+A>").bold().fg(key_color)) .add_col(SpanStatic::raw("<CTRL+A>").bold().fg(key_color))
.add_col(TextSpan::from(" Select all files")) .add_col(SpanStatic::from(" Select all files"))
.add_row() .add_row()
.add_col(TextSpan::new("<ALT+A>").bold().fg(key_color)) .add_col(SpanStatic::raw("<ALT+A>").bold().fg(key_color))
.add_col(TextSpan::from(" Deselect all files")) .add_col(SpanStatic::from(" Deselect all files"))
.add_row() .add_row()
.add_col(TextSpan::new("<CTRL+C>").bold().fg(key_color)) .add_col(SpanStatic::raw("<CTRL+C>").bold().fg(key_color))
.add_col(TextSpan::from(" Interrupt file transfer")) .add_col(SpanStatic::from(" Interrupt file transfer"))
.add_row() .add_row()
.add_col(TextSpan::new("<CTRL+S>").bold().fg(key_color)) .add_col(SpanStatic::raw("<CTRL+S>").bold().fg(key_color))
.add_col(TextSpan::from( .add_col(SpanStatic::from(
" Get total path size of selected files", " Get total path size of selected files",
)) ))
.add_row() .add_row()
.add_col(TextSpan::new("<CTRL+T>").bold().fg(key_color)) .add_col(SpanStatic::raw("<CTRL+T>").bold().fg(key_color))
.add_col(TextSpan::from(" Show watched paths")) .add_col(SpanStatic::from(" Show watched paths"))
.build(), .build()
.into_iter()
.map(|row| row.into_iter().flat_map(|l| l.spans).collect::<Vec<_>>()),
), ),
} }
} }
} }
impl Component<Msg, NoUserEvent> for KeybindingsPopup { impl AppComponent<Msg, NoUserEvent> for KeybindingsPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter, code: Key::Esc | Key::Enter,
@@ -1,12 +1,15 @@
use tui_realm_stdlib::{Input, Radio}; use tui_realm_stdlib::components::{Input, Radio};
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{
BorderType, Borders, Color, HorizontalAlignment, InputType, Style, TextModifiers, Title,
};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, PendingActionMsg, TransferMsg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, PendingActionMsg, TransferMsg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct MkdirPopup { pub struct MkdirPopup {
component: Input, component: Input,
} }
@@ -22,17 +25,17 @@ impl MkdirPopup {
) )
.foreground(color) .foreground(color)
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"New directory name", "New directory name",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
) ))
.title("directory-name", Alignment::Center), .title(Title::from("directory-name").alignment(HorizontalAlignment::Center)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for MkdirPopup { impl AppComponent<Msg, NoUserEvent> for MkdirPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -73,13 +76,13 @@ impl Component<Msg, NoUserEvent> for MkdirPopup {
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
self.perform(Cmd::Type(ch)); self.perform(Cmd::Type(*ch));
Some(Msg::None) Some(Msg::None)
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.state() { }) => match self.state() {
State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::Mkdir(i))), State::Single(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::Mkdir(i))),
_ => Some(Msg::None), _ => Some(Msg::None),
}, },
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
@@ -90,7 +93,7 @@ impl Component<Msg, NoUserEvent> for MkdirPopup {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct SyncBrowsingMkdirPopup { pub struct SyncBrowsingMkdirPopup {
component: Radio, component: Radio,
} }
@@ -99,25 +102,25 @@ impl SyncBrowsingMkdirPopup {
pub fn new(color: Color, dir_name: &str) -> Self { pub fn new(color: Color, dir_name: &str) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(Style::default().fg(color).add_modifier(TextModifiers::REVERSED))
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .choices(["Yes", "No"])
.choices(["Yes", "No"])
.title( .title(
format!( Title::from(format!(
r#"Sync browsing: directory "{dir_name}" doesn't exist. Do you want to create it?"# r#"Sync browsing: directory "{dir_name}" doesn't exist. Do you want to create it?"#
), ))
Alignment::Center, .alignment(HorizontalAlignment::Center),
), ),
} }
} }
} }
impl Component<Msg, NoUserEvent> for SyncBrowsingMkdirPopup { impl AppComponent<Msg, NoUserEvent> for SyncBrowsingMkdirPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -149,7 +152,7 @@ impl Component<Msg, NoUserEvent> for SyncBrowsingMkdirPopup {
}) => { }) => {
if matches!( if matches!(
self.perform(Cmd::Submit), self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0))) CmdResult::Submit(State::Single(StateValue::Usize(0)))
) { ) {
Some(Msg::PendingAction(PendingActionMsg::MakePendingDirectory)) Some(Msg::PendingAction(PendingActionMsg::MakePendingDirectory))
} else { } else {
@@ -1,12 +1,13 @@
use tui_realm_stdlib::Input; use tui_realm_stdlib::components::Input;
use tuirealm::command::{Cmd, Direction, Position}; use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{BorderType, Borders, Color, HorizontalAlignment, InputType, Style, Title};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct NewfilePopup { pub struct NewfilePopup {
component: Input, component: Input,
} }
@@ -22,17 +23,17 @@ impl NewfilePopup {
) )
.foreground(color) .foreground(color)
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"New file name", "New file name",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
) ))
.title("file.txt", Alignment::Center), .title(Title::from("file.txt").alignment(HorizontalAlignment::Center)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for NewfilePopup { impl AppComponent<Msg, NoUserEvent> for NewfilePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -73,13 +74,15 @@ impl Component<Msg, NoUserEvent> for NewfilePopup {
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
self.perform(Cmd::Type(ch)); self.perform(Cmd::Type(*ch));
Some(Msg::None) Some(Msg::None)
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.state() { }) => match self.state() {
State::One(StateValue::String(i)) => Some(Msg::Transfer(TransferMsg::NewFile(i))), State::Single(StateValue::String(i)) => {
Some(Msg::Transfer(TransferMsg::NewFile(i)))
}
_ => Some(Msg::None), _ => Some(Msg::None),
}, },
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
@@ -1,12 +1,13 @@
use tui_realm_stdlib::Input; use tui_realm_stdlib::components::Input;
use tuirealm::command::{Cmd, Direction, Position}; use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{BorderType, Borders, Color, HorizontalAlignment, InputType, Style, Title};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct OpenWithPopup { pub struct OpenWithPopup {
component: Input, component: Input,
} }
@@ -22,17 +23,20 @@ impl OpenWithPopup {
) )
.foreground(color) .foreground(color)
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"Open file with\u{2026}", "Open file with\u{2026}",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
) ))
.title("Type the program to open the file with", Alignment::Center), .title(
Title::from("Type the program to open the file with")
.alignment(HorizontalAlignment::Center),
),
} }
} }
} }
impl Component<Msg, NoUserEvent> for OpenWithPopup { impl AppComponent<Msg, NoUserEvent> for OpenWithPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -73,13 +77,13 @@ impl Component<Msg, NoUserEvent> for OpenWithPopup {
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
self.perform(Cmd::Type(ch)); self.perform(Cmd::Type(*ch));
Some(Msg::None) Some(Msg::None)
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.state() { }) => match self.state() {
State::One(StateValue::String(i)) => { State::Single(StateValue::String(i)) => {
Some(Msg::Transfer(TransferMsg::OpenFileWith(i))) Some(Msg::Transfer(TransferMsg::OpenFileWith(i)))
} }
_ => Some(Msg::None), _ => Some(Msg::None),
@@ -1,30 +1,30 @@
use tui_realm_stdlib::ProgressBar; use tui_realm_stdlib::components::Gauge;
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent}; use tuirealm::props::{BorderType, Borders, Color, HorizontalAlignment, Title};
use crate::ui::activities::filetransfer::{Msg, TransferMsg}; use crate::ui::activities::filetransfer::{Msg, TransferMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct TransferProgressBar { pub struct TransferProgressBar {
component: ProgressBar, component: Gauge,
} }
impl TransferProgressBar { impl TransferProgressBar {
pub fn new<S: Into<String>>(prog: f64, label: S, title: S, color: Color) -> Self { pub fn new<S: Into<String>>(prog: f64, label: S, title: S, color: Color) -> Self {
Self { Self {
component: ProgressBar::default() component: Gauge::default()
.borders(Borders::default().modifiers(BorderType::Rounded)) .borders(Borders::default().modifiers(BorderType::Rounded))
.foreground(color) .foreground(color)
.label(label) .label(label)
.progress(prog) .progress(prog)
.title(title, Alignment::Center), .title(Title::from(title.into()).alignment(HorizontalAlignment::Center)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for TransferProgressBar { impl AppComponent<Msg, NoUserEvent> for TransferProgressBar {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
if matches!( if matches!(
ev, ev,
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
@@ -1,12 +1,15 @@
use tui_realm_stdlib::Radio; use tui_realm_stdlib::components::Radio;
use tuirealm::command::{Cmd, CmdResult, Direction}; use tuirealm::command::{Cmd, CmdResult, Direction};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{
BorderType, Borders, Color, HorizontalAlignment, Style, TextModifiers, Title,
};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct QuitPopup { pub struct QuitPopup {
component: Radio, component: Radio,
} }
@@ -15,20 +18,27 @@ impl QuitPopup {
pub fn new(color: Color) -> Self { pub fn new(color: Color) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color)
.choices(["Yes", "No"]) .choices(["Yes", "No"])
.title("Are you sure you want to quit termscp?", Alignment::Center), .title(
Title::from("Are you sure you want to quit termscp?")
.alignment(HorizontalAlignment::Center),
),
} }
} }
} }
impl Component<Msg, NoUserEvent> for QuitPopup { impl AppComponent<Msg, NoUserEvent> for QuitPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -58,7 +68,7 @@ impl Component<Msg, NoUserEvent> for QuitPopup {
}) => { }) => {
if matches!( if matches!(
self.perform(Cmd::Submit), self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0))) CmdResult::Submit(State::Single(StateValue::Usize(0)))
) { ) {
Some(Msg::Ui(UiMsg::Quit)) Some(Msg::Ui(UiMsg::Quit))
} else { } else {
@@ -1,12 +1,13 @@
use tui_realm_stdlib::Input; use tui_realm_stdlib::components::Input;
use tuirealm::command::{Cmd, Direction, Position}; use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{BorderType, Borders, Color, HorizontalAlignment, InputType, Style, Title};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct RenamePopup { pub struct RenamePopup {
component: Input, component: Input,
} }
@@ -22,17 +23,19 @@ impl RenamePopup {
) )
.foreground(color) .foreground(color)
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"/foo/bar/buzz.txt", "/foo/bar/buzz.txt",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
) ))
.title("Move file(s) to\u{2026}", Alignment::Center), .title(
Title::from("Move file(s) to\u{2026}").alignment(HorizontalAlignment::Center),
),
} }
} }
} }
impl Component<Msg, NoUserEvent> for RenamePopup { impl AppComponent<Msg, NoUserEvent> for RenamePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -73,13 +76,13 @@ impl Component<Msg, NoUserEvent> for RenamePopup {
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
self.perform(Cmd::Type(ch)); self.perform(Cmd::Type(*ch));
Some(Msg::None) Some(Msg::None)
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.state() { }) => match self.state() {
State::One(StateValue::String(i)) => { State::Single(StateValue::String(i)) => {
Some(Msg::Transfer(TransferMsg::RenameFile(i))) Some(Msg::Transfer(TransferMsg::RenameFile(i)))
} }
_ => Some(Msg::None), _ => Some(Msg::None),
@@ -1,12 +1,15 @@
use tui_realm_stdlib::Radio; use tui_realm_stdlib::components::Radio;
use tuirealm::command::{Cmd, CmdResult, Direction}; use tuirealm::command::{Cmd, CmdResult, Direction};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{
BorderType, Borders, Color, HorizontalAlignment, Style, TextModifiers, Title,
};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, PendingActionMsg}; use crate::ui::activities::filetransfer::{Msg, PendingActionMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct ReplacePopup { pub struct ReplacePopup {
component: Radio, component: Radio,
} }
@@ -19,20 +22,24 @@ impl ReplacePopup {
}; };
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color)
.choices(["Replace", "Skip", "Replace All", "Skip All", "Cancel"]) .choices(["Replace", "Skip", "Replace All", "Skip All", "Cancel"])
.title(text, Alignment::Center), .title(Title::from(text).alignment(HorizontalAlignment::Center)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for ReplacePopup { impl AppComponent<Msg, NoUserEvent> for ReplacePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -60,19 +67,19 @@ impl Component<Msg, NoUserEvent> for ReplacePopup {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.perform(Cmd::Submit) { }) => match self.perform(Cmd::Submit) {
CmdResult::Submit(State::One(StateValue::Usize(0))) => { CmdResult::Submit(State::Single(StateValue::Usize(0))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite)) Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwrite))
} }
CmdResult::Submit(State::One(StateValue::Usize(1))) => { CmdResult::Submit(State::Single(StateValue::Usize(1))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip)) Some(Msg::PendingAction(PendingActionMsg::ReplaceSkip))
} }
CmdResult::Submit(State::One(StateValue::Usize(2))) => { CmdResult::Submit(State::Single(StateValue::Usize(2))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll)) Some(Msg::PendingAction(PendingActionMsg::ReplaceOverwriteAll))
} }
CmdResult::Submit(State::One(StateValue::Usize(3))) => { CmdResult::Submit(State::Single(StateValue::Usize(3))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceSkipAll)) Some(Msg::PendingAction(PendingActionMsg::ReplaceSkipAll))
} }
CmdResult::Submit(State::One(StateValue::Usize(4))) => { CmdResult::Submit(State::Single(StateValue::Usize(4))) => {
Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel)) Some(Msg::PendingAction(PendingActionMsg::ReplaceCancel))
} }
_ => Some(Msg::None), _ => Some(Msg::None),
@@ -1,12 +1,13 @@
use tui_realm_stdlib::Input; use tui_realm_stdlib::components::Input;
use tuirealm::command::{Cmd, Direction, Position}; use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{BorderType, Borders, Color, HorizontalAlignment, InputType, Style, Title};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct SaveAsPopup { pub struct SaveAsPopup {
component: Input, component: Input,
} }
@@ -22,17 +23,17 @@ impl SaveAsPopup {
) )
.foreground(color) .foreground(color)
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"/foo/bar/buzz.txt", "/foo/bar/buzz.txt",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
) ))
.title("Save as\u{2026}", Alignment::Center), .title(Title::from("Save as\u{2026}").alignment(HorizontalAlignment::Center)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for SaveAsPopup { impl AppComponent<Msg, NoUserEvent> for SaveAsPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -73,13 +74,13 @@ impl Component<Msg, NoUserEvent> for SaveAsPopup {
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
self.perform(Cmd::Type(ch)); self.perform(Cmd::Type(*ch));
Some(Msg::None) Some(Msg::None)
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.state() { }) => match self.state() {
State::One(StateValue::String(i)) => { State::Single(StateValue::String(i)) => {
Some(Msg::Transfer(TransferMsg::SaveFileAs(i))) Some(Msg::Transfer(TransferMsg::SaveFileAs(i)))
} }
_ => Some(Msg::None), _ => Some(Msg::None),
@@ -1,13 +1,16 @@
use tui_realm_stdlib::Radio; use tui_realm_stdlib::components::Radio;
use tuirealm::command::{Cmd, CmdResult, Direction}; use tuirealm::command::{Cmd, CmdResult, Direction};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{
BorderType, Borders, Color, HorizontalAlignment, Style, TextModifiers, Title,
};
use tuirealm::state::{State, StateValue};
use crate::explorer::FileSorting; use crate::explorer::FileSorting;
use crate::ui::activities::filetransfer::{Msg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct SortingPopup { pub struct SortingPopup {
component: Radio, component: Radio,
} }
@@ -16,14 +19,18 @@ impl SortingPopup {
pub fn new(value: FileSorting, color: Color) -> Self { pub fn new(value: FileSorting, color: Color) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color)
.choices(["Name", "Modify time", "Creation time", "Size"]) .choices(["Name", "Modify time", "Creation time", "Size"])
.title("Sort files by\u{2026}", Alignment::Center) .title(Title::from("Sort files by\u{2026}").alignment(HorizontalAlignment::Center))
.value(match value { .value(match value {
FileSorting::CreationTime => 2, FileSorting::CreationTime => 2,
FileSorting::ModifyTime => 1, FileSorting::ModifyTime => 1,
@@ -35,8 +42,8 @@ impl SortingPopup {
} }
} }
impl Component<Msg, NoUserEvent> for SortingPopup { impl AppComponent<Msg, NoUserEvent> for SortingPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
let result = match ev { let result = match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -50,7 +57,7 @@ impl Component<Msg, NoUserEvent> for SortingPopup {
}) => return Some(Msg::Ui(UiMsg::CloseFileSortingPopup)), }) => return Some(Msg::Ui(UiMsg::CloseFileSortingPopup)),
_ => return None, _ => return None,
}; };
if let CmdResult::Changed(State::One(StateValue::Usize(i))) = result { if let CmdResult::Changed(State::Single(StateValue::Usize(i))) = result {
Some(Msg::Ui(UiMsg::ChangeFileSorting(match i { Some(Msg::Ui(UiMsg::ChangeFileSorting(match i {
0 => FileSorting::Name, 0 => FileSorting::Name,
1 => FileSorting::ModifyTime, 1 => FileSorting::ModifyTime,
@@ -1,12 +1,14 @@
use tui_realm_stdlib::Span; use tui_realm_stdlib::components::Span;
use tuirealm::props::{Color, TextSpan}; use tuirealm::component::{AppComponent, Component};
use tuirealm::{Component, Event, MockComponent, NoUserEvent}; use tuirealm::event::{Event, NoUserEvent};
use tuirealm::props::{Color, SpanStatic};
use tuirealm::ratatui::style::Stylize;
use crate::explorer::FileSorting; use crate::explorer::FileSorting;
use crate::ui::activities::filetransfer::Msg; use crate::ui::activities::filetransfer::Msg;
use crate::ui::activities::filetransfer::lib::browser::Browser; use crate::ui::activities::filetransfer::lib::browser::Browser;
#[derive(MockComponent)] #[derive(Component)]
pub struct StatusBarLocal { pub struct StatusBarLocal {
component: Span, component: Span,
} }
@@ -17,22 +19,22 @@ impl StatusBarLocal {
let hidden_files = hidden_files_label(browser.host_bridge().hidden_files_visible()); let hidden_files = hidden_files_label(browser.host_bridge().hidden_files_visible());
Self { Self {
component: Span::default().spans([ component: Span::default().spans([
TextSpan::new("File sorting: ").fg(sorting_color), SpanStatic::raw("File sorting: ").fg(sorting_color),
TextSpan::new(file_sorting).fg(sorting_color).reversed(), SpanStatic::raw(file_sorting).fg(sorting_color).reversed(),
TextSpan::new(" Hidden files: ").fg(hidden_color), SpanStatic::raw(" Hidden files: ").fg(hidden_color),
TextSpan::new(hidden_files).fg(hidden_color).reversed(), SpanStatic::raw(hidden_files).fg(hidden_color).reversed(),
]), ]),
} }
} }
} }
impl Component<Msg, NoUserEvent> for StatusBarLocal { impl AppComponent<Msg, NoUserEvent> for StatusBarLocal {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct StatusBarRemote { pub struct StatusBarRemote {
component: Span, component: Span,
} }
@@ -52,19 +54,19 @@ impl StatusBarRemote {
}; };
Self { Self {
component: Span::default().spans([ component: Span::default().spans([
TextSpan::new("File sorting: ").fg(sorting_color), SpanStatic::raw("File sorting: ").fg(sorting_color),
TextSpan::new(file_sorting).fg(sorting_color).reversed(), SpanStatic::raw(file_sorting).fg(sorting_color).reversed(),
TextSpan::new(" Hidden files: ").fg(hidden_color), SpanStatic::raw(" Hidden files: ").fg(hidden_color),
TextSpan::new(hidden_files).fg(hidden_color).reversed(), SpanStatic::raw(hidden_files).fg(hidden_color).reversed(),
TextSpan::new(" Sync browsing: ").fg(sync_color), SpanStatic::raw(" Sync browsing: ").fg(sync_color),
TextSpan::new(sync_browsing).fg(sync_color).reversed(), SpanStatic::raw(sync_browsing).fg(sync_color).reversed(),
]), ]),
} }
} }
} }
impl Component<Msg, NoUserEvent> for StatusBarRemote { impl AppComponent<Msg, NoUserEvent> for StatusBarRemote {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
@@ -1,12 +1,13 @@
use tui_realm_stdlib::Input; use tui_realm_stdlib::components::Input;
use tuirealm::command::{Cmd, Direction, Position}; use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{BorderType, Borders, Color, HorizontalAlignment, InputType, Style, Title};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct SymlinkPopup { pub struct SymlinkPopup {
component: Input, component: Input,
} }
@@ -22,20 +23,20 @@ impl SymlinkPopup {
) )
.foreground(color) .foreground(color)
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"Symlink name", "Symlink name",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
) ))
.title( .title(
"Create a symlink pointing to the selected entry", Title::from("Create a symlink pointing to the selected entry")
Alignment::Center, .alignment(HorizontalAlignment::Center),
), ),
} }
} }
} }
impl Component<Msg, NoUserEvent> for SymlinkPopup { impl AppComponent<Msg, NoUserEvent> for SymlinkPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -76,13 +77,13 @@ impl Component<Msg, NoUserEvent> for SymlinkPopup {
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
self.perform(Cmd::Type(ch)); self.perform(Cmd::Type(*ch));
Some(Msg::None) Some(Msg::None)
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.state() { }) => match self.state() {
State::One(StateValue::String(i)) => { State::Single(StateValue::String(i)) => {
Some(Msg::Transfer(TransferMsg::CreateSymlink(i))) Some(Msg::Transfer(TransferMsg::CreateSymlink(i)))
} }
_ => Some(Msg::None), _ => Some(Msg::None),
@@ -1,11 +1,12 @@
use tui_realm_stdlib::Paragraph; use tui_realm_stdlib::components::Paragraph;
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent}; use tuirealm::props::{BorderType, Borders, Color, HorizontalAlignment, SpanStatic};
use tuirealm::ratatui::text::Text;
use crate::ui::activities::filetransfer::{Msg, TransferMsg}; use crate::ui::activities::filetransfer::{Msg, TransferMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct WaitPopup { pub struct WaitPopup {
component: Paragraph, component: Paragraph,
} }
@@ -14,26 +15,28 @@ impl WaitPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self { pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self { Self {
component: Paragraph::default() component: Paragraph::default()
.alignment(Alignment::Center) .alignment_horizontal(HorizontalAlignment::Center)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.text([TextSpan::from(text.as_ref())]) .text(Text::from_iter([SpanStatic::from(
.wrap(true), text.as_ref().to_string(),
)]))
.wrap_trim(true),
} }
} }
} }
impl Component<Msg, NoUserEvent> for WaitPopup { impl AppComponent<Msg, NoUserEvent> for WaitPopup {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct WalkdirWaitPopup { pub struct WalkdirWaitPopup {
component: Paragraph, component: Paragraph,
} }
@@ -42,24 +45,24 @@ impl WalkdirWaitPopup {
pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self { pub fn new<S: AsRef<str>>(text: S, color: Color) -> Self {
Self { Self {
component: Paragraph::default() component: Paragraph::default()
.alignment(Alignment::Center) .alignment_horizontal(HorizontalAlignment::Center)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color) .foreground(color)
.text([ .text(Text::from_iter([
TextSpan::from(text.as_ref()), SpanStatic::from(text.as_ref().to_string()),
TextSpan::from("Press 'CTRL+C' to abort"), SpanStatic::from("Press 'CTRL+C' to abort"),
]) ]))
.wrap(true), .wrap_trim(true),
} }
} }
} }
impl Component<Msg, NoUserEvent> for WalkdirWaitPopup { impl AppComponent<Msg, NoUserEvent> for WalkdirWaitPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
if matches!( if matches!(
ev, ev,
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
@@ -1,12 +1,15 @@
use tui_realm_stdlib::{List, Radio}; use tui_realm_stdlib::components::{List, Radio};
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{
BorderType, Borders, Color, HorizontalAlignment, SpanStatic, Style, TextModifiers, Title,
};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg}; use crate::ui::activities::filetransfer::{Msg, TransferMsg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct WatchedPathsList { pub struct WatchedPathsList {
component: List, component: List,
} }
@@ -23,24 +26,24 @@ impl WatchedPathsList {
.rewind(true) .rewind(true)
.scroll(true) .scroll(true)
.step(4) .step(4)
.highlighted_color(color) .highlight_style(Style::default().fg(color))
.highlighted_str("\u{27a4} ") .highlight_str("\u{27a4} ")
.title( .title(
"These files are currently synched with the remote host", Title::from("These files are currently synched with the remote host")
Alignment::Center, .alignment(HorizontalAlignment::Center),
) )
.rows( .rows(
paths paths
.iter() .iter()
.map(|x| vec![TextSpan::from(x.to_string_lossy().to_string())]) .map(|x| vec![SpanStatic::from(x.to_string_lossy().to_string())])
.collect(), .collect::<Vec<Vec<SpanStatic>>>(),
), ),
} }
} }
} }
impl Component<Msg, NoUserEvent> for WatchedPathsList { impl AppComponent<Msg, NoUserEvent> for WatchedPathsList {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseWatchedPathsList)) Some(Msg::Ui(UiMsg::CloseWatchedPathsList))
@@ -82,7 +85,7 @@ impl Component<Msg, NoUserEvent> for WatchedPathsList {
code: Key::Enter, .. code: Key::Enter, ..
}) => { }) => {
// get state // get state
if let State::One(StateValue::Usize(idx)) = self.component.state() { if let State::Single(StateValue::Usize(idx)) = self.component.state() {
Some(Msg::Transfer(TransferMsg::ToggleWatchFor(idx))) Some(Msg::Transfer(TransferMsg::ToggleWatchFor(idx)))
} else { } else {
Some(Msg::None) Some(Msg::None)
@@ -93,7 +96,7 @@ impl Component<Msg, NoUserEvent> for WatchedPathsList {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct WatcherPopup { pub struct WatcherPopup {
component: Radio, component: Radio,
} }
@@ -106,20 +109,24 @@ impl WatcherPopup {
}; };
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(color)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(color) .color(color)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(color)
.choices(["Yes", "No"]) .choices(["Yes", "No"])
.title(text, Alignment::Center), .title(Title::from(text).alignment(HorizontalAlignment::Center)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for WatcherPopup { impl AppComponent<Msg, NoUserEvent> for WatcherPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -149,7 +156,7 @@ impl Component<Msg, NoUserEvent> for WatcherPopup {
}) => { }) => {
if matches!( if matches!(
self.perform(Cmd::Submit), self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0))) CmdResult::Submit(State::Single(StateValue::Usize(0)))
) { ) {
Some(Msg::Transfer(TransferMsg::ToggleWatch)) Some(Msg::Transfer(TransferMsg::ToggleWatch))
} else { } else {
@@ -1,14 +1,15 @@
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use tui_realm_stdlib::List; use tui_realm_stdlib::components::List;
use tuirealm::command::{Cmd, Direction, Position}; use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, TextSpan}; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{BorderType, Borders, Color, HorizontalAlignment, SpanStatic, Style, Title};
use tuirealm::state::{State, StateValue};
use crate::ui::activities::filetransfer::{MarkQueue, Msg, UiMsg}; use crate::ui::activities::filetransfer::{MarkQueue, Msg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct SelectedFilesList { pub struct SelectedFilesList {
component: List, component: List,
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
@@ -35,20 +36,20 @@ impl SelectedFilesList {
.rewind(true) .rewind(true)
.scroll(true) .scroll(true)
.step(4) .step(4)
.highlighted_color(color) .highlight_style(Style::default().fg(color))
.highlighted_str("➤ ") .highlight_str("➤ ")
.title(title, Alignment::Left) .title(Title::from(title.to_string()).alignment(HorizontalAlignment::Left))
.rows( .rows(
paths paths
.iter() .iter()
.map(|(src, dest)| { .map(|(src, dest)| {
vec![ vec![
TextSpan::from(Self::filename(src)), SpanStatic::from(Self::filename(src)),
TextSpan::from(" -> "), SpanStatic::from(" -> "),
TextSpan::from(Self::filename(dest)), SpanStatic::from(Self::filename(dest)),
] ]
}) })
.collect(), .collect::<Vec<Vec<SpanStatic>>>(),
), ),
} }
} }
@@ -61,8 +62,8 @@ impl SelectedFilesList {
} }
} }
impl Component<Msg, NoUserEvent> for SelectedFilesList { impl AppComponent<Msg, NoUserEvent> for SelectedFilesList {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Down, .. code: Key::Down, ..
@@ -112,7 +113,7 @@ impl Component<Msg, NoUserEvent> for SelectedFilesList {
.. ..
}) => { }) => {
// unmark the selected file // unmark the selected file
let State::One(StateValue::Usize(idx)) = self.state() else { let State::Single(StateValue::Usize(idx)) = self.state() else {
return None; return None;
}; };
@@ -3,16 +3,16 @@ mod history;
mod line; mod line;
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::Color; use tuirealm::event::{Event, Key, KeyEvent, NoUserEvent};
use tuirealm::{AttrValue, Attribute, Component, Event, MockComponent, NoUserEvent}; use tuirealm::props::{AttrValue, Attribute, Color};
use self::component::TerminalComponent; use self::component::TerminalComponent;
use self::line::Line; use self::line::Line;
use super::Msg; use super::Msg;
use crate::ui::activities::filetransfer::{TransferMsg, UiMsg}; use crate::ui::activities::filetransfer::{TransferMsg, UiMsg};
#[derive(MockComponent, Default)] #[derive(Component, Default)]
pub struct Terminal { pub struct Terminal {
component: TerminalComponent, component: TerminalComponent,
} }
@@ -45,8 +45,8 @@ impl Terminal {
} }
} }
impl Component<Msg, NoUserEvent> for Terminal { impl AppComponent<Msg, NoUserEvent> for Terminal {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ui(UiMsg::CloseExecPopup)) Some(Msg::Ui(UiMsg::CloseExecPopup))
@@ -55,7 +55,7 @@ impl Component<Msg, NoUserEvent> for Terminal {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.component.perform(Cmd::Submit) { }) => match self.component.perform(Cmd::Submit) {
CmdResult::Submit(state) => { CmdResult::Submit(state) => {
let cmd = state.unwrap_one().unwrap_string(); let cmd = state.unwrap_single().unwrap_string();
Some(Msg::Transfer(TransferMsg::ExecuteCmd(cmd))) Some(Msg::Transfer(TransferMsg::ExecuteCmd(cmd)))
} }
_ => None, _ => None,
@@ -127,7 +127,7 @@ impl Component<Msg, NoUserEvent> for Terminal {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Char(c), .. code: Key::Char(c), ..
}) => { }) => {
self.component.perform(Cmd::Type(c)); self.component.perform(Cmd::Type(*c));
Some(Msg::None) Some(Msg::None)
} }
_ => None, _ => None,
@@ -1,10 +1,11 @@
use tui_term::vt100::Parser; use tui_term::vt100::Parser;
use tui_term::widget::PseudoTerminal; use tui_term::widget::PseudoTerminal;
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::props::{BorderSides, BorderType, Style}; use tuirealm::component::Component;
use tuirealm::props::{AttrValue, Attribute, BorderSides, BorderType, Props, QueryResult, Style};
use tuirealm::ratatui::layout::Rect; use tuirealm::ratatui::layout::Rect;
use tuirealm::ratatui::widgets::Block; use tuirealm::ratatui::widgets::Block;
use tuirealm::{AttrValue, Attribute, MockComponent, Props, State, StateValue}; use tuirealm::state::{State, StateValue};
use super::Line; use super::Line;
use super::history::History; use super::history::History;
@@ -45,8 +46,7 @@ impl TerminalComponent {
} }
pub fn write_prompt(&mut self) { pub fn write_prompt(&mut self) {
if let Some(value) = self.query(Attribute::Content) { if let Some(AttrValue::String(prompt)) = self.props.get(Attribute::Content).cloned() {
let prompt = value.unwrap_string();
self.parser.process(prompt.as_bytes()); self.parser.process(prompt.as_bytes());
} }
} }
@@ -83,8 +83,8 @@ impl TerminalComponent {
} }
} }
impl MockComponent for TerminalComponent { impl Component for TerminalComponent {
fn view(&mut self, frame: &mut tuirealm::Frame, area: Rect) { fn view(&mut self, frame: &mut tuirealm::ratatui::Frame, area: Rect) {
let width = area.width.saturating_sub(2); let width = area.width.saturating_sub(2);
let height = area.height.saturating_sub(2); let height = area.height.saturating_sub(2);
@@ -95,23 +95,30 @@ impl MockComponent for TerminalComponent {
} }
let title = self let title = self
.query(Attribute::Title) .props
.map(AttrValue::unwrap_string) .get(Attribute::Title)
.and_then(|v| match v {
AttrValue::String(s) => Some(s.clone()),
_ => None,
})
.unwrap_or_else(|| "Terminal".to_string()); .unwrap_or_else(|| "Terminal".to_string());
let fg = self let fg = self
.query(Attribute::Foreground) .props
.map(AttrValue::unwrap_color) .get(Attribute::Foreground)
.and_then(AttrValue::as_color)
.unwrap_or(tuirealm::ratatui::style::Color::Reset); .unwrap_or(tuirealm::ratatui::style::Color::Reset);
let bg = self let bg = self
.query(Attribute::Background) .props
.map(AttrValue::unwrap_color) .get(Attribute::Background)
.and_then(AttrValue::as_color)
.unwrap_or(tuirealm::ratatui::style::Color::Reset); .unwrap_or(tuirealm::ratatui::style::Color::Reset);
let border_color = self let border_color = self
.query(Attribute::Borders) .props
.map(AttrValue::unwrap_color) .get(Attribute::Borders)
.and_then(AttrValue::as_color)
.unwrap_or(tuirealm::ratatui::style::Color::Reset); .unwrap_or(tuirealm::ratatui::style::Color::Reset);
let terminal = PseudoTerminal::new(self.parser.screen()) let terminal = PseudoTerminal::new(self.parser.screen())
@@ -128,13 +135,13 @@ impl MockComponent for TerminalComponent {
frame.render_widget(terminal, area); frame.render_widget(terminal, area);
} }
fn query(&self, attr: tuirealm::Attribute) -> Option<tuirealm::AttrValue> { fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
self.props.get(attr) self.props.get_for_query(attr)
} }
fn attr(&mut self, attr: tuirealm::Attribute, value: AttrValue) { fn attr(&mut self, attr: Attribute, value: AttrValue) {
if attr == Attribute::Text { if attr == Attribute::Text {
if let tuirealm::AttrValue::String(s) = value { if let AttrValue::String(s) = value {
self.parser.process(b"\r"); self.parser.process(b"\r");
self.parser.process(s.as_bytes()); self.parser.process(s.as_bytes());
self.parser.process(b"\r"); self.parser.process(b"\r");
@@ -146,14 +153,14 @@ impl MockComponent for TerminalComponent {
} }
fn state(&self) -> State { fn state(&self) -> State {
State::One(StateValue::String(self.line.content().to_string())) State::Single(StateValue::String(self.line.content().to_string()))
} }
fn perform(&mut self, cmd: Cmd) -> CmdResult { fn perform(&mut self, cmd: Cmd) -> CmdResult {
match cmd { match cmd {
Cmd::Type(s) => { Cmd::Type(s) => {
if !s.is_ascii() || self.scroll > 0 { if !s.is_ascii() || self.scroll > 0 {
return CmdResult::None; // Ignore non-ASCII characters or if scrolled return CmdResult::NoChange; // Ignore non-ASCII characters or if scrolled
} }
self.parser.process(&[s as u8]); self.parser.process(&[s as u8]);
self.line.push(s); self.line.push(s);
@@ -161,46 +168,46 @@ impl MockComponent for TerminalComponent {
} }
Cmd::Move(Direction::Down) => { Cmd::Move(Direction::Down) => {
if self.scroll > 0 { if self.scroll > 0 {
return CmdResult::None; // Cannot move down if not scrolled return CmdResult::NoChange; // Cannot move down if not scrolled
} }
self.history_next(); self.history_next();
CmdResult::None CmdResult::NoChange
} }
Cmd::Move(Direction::Left) => { Cmd::Move(Direction::Left) => {
if self.scroll > 0 { if self.scroll > 0 {
return CmdResult::None; // Cannot move up if not scrolled return CmdResult::NoChange; // Cannot move up if not scrolled
} }
if self.line.left() { if self.line.left() {
self.parser.process(&[27, 91, 68]); self.parser.process(&[27, 91, 68]);
} }
CmdResult::None CmdResult::NoChange
} }
Cmd::Move(Direction::Right) => { Cmd::Move(Direction::Right) => {
if self.scroll > 0 { if self.scroll > 0 {
return CmdResult::None; // Cannot move up if not scrolled return CmdResult::NoChange; // Cannot move up if not scrolled
} }
if self.line.right() { if self.line.right() {
self.parser.process(&[27, 91, 67]); self.parser.process(&[27, 91, 67]);
} }
CmdResult::None CmdResult::NoChange
} }
Cmd::Move(Direction::Up) => { Cmd::Move(Direction::Up) => {
if self.scroll > 0 { if self.scroll > 0 {
return CmdResult::None; // Cannot move up if not scrolled return CmdResult::NoChange; // Cannot move up if not scrolled
} }
self.history_prev(); self.history_prev();
CmdResult::None CmdResult::NoChange
} }
Cmd::Cancel => { Cmd::Cancel => {
if self.scroll > 0 { if self.scroll > 0 {
return CmdResult::None; // Cannot move to the beginning if scrolled return CmdResult::NoChange; // Cannot move to the beginning if scrolled
} }
if !self.line.is_empty() { if !self.line.is_empty() {
@@ -215,7 +222,7 @@ impl MockComponent for TerminalComponent {
} }
Cmd::Delete => { Cmd::Delete => {
if self.scroll > 0 { if self.scroll > 0 {
return CmdResult::None; // Cannot move to the beginning if scrolled return CmdResult::NoChange; // Cannot move to the beginning if scrolled
} }
if !self.line.is_empty() { if !self.line.is_empty() {
@@ -231,40 +238,40 @@ impl MockComponent for TerminalComponent {
self.scroll = self.scroll.saturating_sub(8); self.scroll = self.scroll.saturating_sub(8);
self.parser.set_scrollback(self.scroll); self.parser.set_scrollback(self.scroll);
CmdResult::None CmdResult::NoChange
} }
Cmd::Scroll(Direction::Up) => { Cmd::Scroll(Direction::Up) => {
self.parser.set_scrollback(self.scroll.saturating_add(8)); self.parser.set_scrollback(self.scroll.saturating_add(8));
let scrollback = self.parser.screen().scrollback(); let scrollback = self.parser.screen().scrollback();
self.scroll = scrollback; self.scroll = scrollback;
CmdResult::None CmdResult::NoChange
} }
Cmd::Toggle => { Cmd::Toggle => {
// insert // insert
self.parser.process(&[27, 91, 50, 126]); // Toggle insert mode self.parser.process(&[27, 91, 50, 126]); // Toggle insert mode
CmdResult::None CmdResult::NoChange
} }
Cmd::GoTo(Position::Begin) => { Cmd::GoTo(Position::Begin) => {
if self.scroll > 0 { if self.scroll > 0 {
return CmdResult::None; // Cannot move to the beginning if scrolled return CmdResult::NoChange; // Cannot move to the beginning if scrolled
} }
for _ in 0..self.line.begin() { for _ in 0..self.line.begin() {
self.parser.process(&[27, 91, 68]); // Move cursor to the left self.parser.process(&[27, 91, 68]); // Move cursor to the left
} }
CmdResult::None CmdResult::NoChange
} }
Cmd::GoTo(Position::End) => { Cmd::GoTo(Position::End) => {
if self.scroll > 0 { if self.scroll > 0 {
return CmdResult::None; // Cannot move to the beginning if scrolled return CmdResult::NoChange; // Cannot move to the beginning if scrolled
} }
for _ in 0..self.line.end() { for _ in 0..self.line.end() {
self.parser.process(&[27, 91, 67]); // Move cursor to the right self.parser.process(&[27, 91, 67]); // Move cursor to the right
} }
CmdResult::None CmdResult::NoChange
} }
Cmd::Submit => { Cmd::Submit => {
self.scroll = 0; // Reset scroll on submit self.scroll = 0; // Reset scroll on submit
@@ -281,9 +288,9 @@ impl MockComponent for TerminalComponent {
self.history.push(&line); self.history.push(&line);
} }
CmdResult::Submit(State::One(StateValue::String(line))) CmdResult::Submit(State::Single(StateValue::String(line)))
} }
_ => CmdResult::None, _ => CmdResult::NoChange,
} }
} }
} }
@@ -6,15 +6,16 @@ mod file_list;
mod file_list_with_search; mod file_list_with_search;
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, Borders, Color, TextSpan}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{Borders, Color, HorizontalAlignment, SpanStatic, Title};
use tuirealm::state::{State, StateValue};
use self::file_list::FileList; use self::file_list::FileList;
use self::file_list_with_search::FileListWithSearch; use self::file_list_with_search::FileListWithSearch;
use super::{Msg, TransferMsg, UiMsg}; use super::{Msg, TransferMsg, UiMsg};
#[derive(MockComponent)] #[derive(Component)]
pub struct ExplorerFuzzy { pub struct ExplorerFuzzy {
component: FileListWithSearch, component: FileListWithSearch,
} }
@@ -26,13 +27,22 @@ impl ExplorerFuzzy {
.background(bg) .background(bg)
.borders(Borders::default().color(hg)) .borders(Borders::default().color(hg))
.foreground(fg) .foreground(fg)
.highlighted_color(hg) .highlight_color(hg)
.title(title, Alignment::Left) .title(Title::from(title.as_ref().to_string()).alignment(HorizontalAlignment::Left))
.rows(files.iter().map(|x| vec![TextSpan::from(*x)]).collect()), .rows(
files
.iter()
.map(|x| {
vec![tuirealm::props::LineStatic::from(SpanStatic::from(
(*x).to_string(),
))]
})
.collect(),
),
} }
} }
fn on_search(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on_search(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -79,8 +89,8 @@ impl ExplorerFuzzy {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => match self.perform(Cmd::Type(ch)) { }) => match self.perform(Cmd::Type(*ch)) {
CmdResult::Changed(State::One(StateValue::String(search))) => { CmdResult::Changed(State::Single(StateValue::String(search))) => {
Some(Msg::Ui(UiMsg::FuzzySearch(search))) Some(Msg::Ui(UiMsg::FuzzySearch(search)))
} }
_ => Some(Msg::None), _ => Some(Msg::None),
@@ -92,7 +102,7 @@ impl ExplorerFuzzy {
} }
} }
fn on_file_list(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on_file_list(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Down, .. code: Key::Down, ..
@@ -145,7 +155,7 @@ impl ExplorerFuzzy {
code: Key::Char('m'), code: Key::Char('m'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}) => { }) => {
let CmdResult::Changed(State::One(StateValue::Usize(index))) = let CmdResult::Changed(State::Single(StateValue::Usize(index))) =
self.perform(Cmd::Toggle) self.perform(Cmd::Toggle)
else { else {
return Some(Msg::None); return Some(Msg::None);
@@ -217,8 +227,8 @@ impl ExplorerFuzzy {
} }
} }
impl Component<Msg, NoUserEvent> for ExplorerFuzzy { impl AppComponent<Msg, NoUserEvent> for ExplorerFuzzy {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match self.component.focus() { match self.component.focus() {
file_list_with_search::Focus::List => self.on_file_list(ev), file_list_with_search::Focus::List => self.on_file_list(ev),
file_list_with_search::Focus::Search => self.on_search(ev), file_list_with_search::Focus::Search => self.on_search(ev),
@@ -226,7 +236,7 @@ impl Component<Msg, NoUserEvent> for ExplorerFuzzy {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct ExplorerFind { pub struct ExplorerFind {
component: FileList, component: FileList,
} }
@@ -238,15 +248,24 @@ impl ExplorerFind {
.background(bg) .background(bg)
.borders(Borders::default().color(hg)) .borders(Borders::default().color(hg))
.foreground(fg) .foreground(fg)
.highlighted_color(hg) .highlight_color(hg)
.title(title, Alignment::Left) .title(Title::from(title.as_ref().to_string()).alignment(HorizontalAlignment::Left))
.rows(files.iter().map(|x| vec![TextSpan::from(*x)]).collect()), .rows(
files
.iter()
.map(|x| {
vec![tuirealm::props::LineStatic::from(SpanStatic::from(
(*x).to_string(),
))]
})
.collect(),
),
} }
} }
} }
impl Component<Msg, NoUserEvent> for ExplorerFind { impl AppComponent<Msg, NoUserEvent> for ExplorerFind {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Down, .. code: Key::Down, ..
@@ -299,7 +318,7 @@ impl Component<Msg, NoUserEvent> for ExplorerFind {
code: Key::Char('m'), code: Key::Char('m'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}) => { }) => {
let CmdResult::Changed(State::One(StateValue::Usize(index))) = let CmdResult::Changed(State::Single(StateValue::Usize(index))) =
self.perform(Cmd::Toggle) self.perform(Cmd::Toggle)
else { else {
return Some(Msg::None); return Some(Msg::None);
@@ -367,7 +386,7 @@ impl Component<Msg, NoUserEvent> for ExplorerFind {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct ExplorerLocal { pub struct ExplorerLocal {
component: FileList, component: FileList,
} }
@@ -379,16 +398,25 @@ impl ExplorerLocal {
.background(bg) .background(bg)
.borders(Borders::default().color(hg)) .borders(Borders::default().color(hg))
.foreground(fg) .foreground(fg)
.highlighted_color(hg) .highlight_color(hg)
.title(title, Alignment::Left) .title(Title::from(title.as_ref().to_string()).alignment(HorizontalAlignment::Left))
.rows(files.iter().map(|x| vec![TextSpan::from(*x)]).collect()) .rows(
files
.iter()
.map(|x| {
vec![tuirealm::props::LineStatic::from(SpanStatic::from(
(*x).to_string(),
))]
})
.collect(),
)
.dot_dot(true), .dot_dot(true),
} }
} }
} }
impl Component<Msg, NoUserEvent> for ExplorerLocal { impl AppComponent<Msg, NoUserEvent> for ExplorerLocal {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Down, .. code: Key::Down, ..
@@ -441,7 +469,7 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
code: Key::Char('m'), code: Key::Char('m'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}) => { }) => {
let CmdResult::Changed(State::One(StateValue::Usize(index))) = let CmdResult::Changed(State::Single(StateValue::Usize(index))) =
self.perform(Cmd::Toggle) self.perform(Cmd::Toggle)
else { else {
return Some(Msg::None); return Some(Msg::None);
@@ -464,7 +492,7 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => { }) => {
if matches!(self.component.state(), State::One(StateValue::String(_))) { if matches!(self.component.state(), State::Single(StateValue::String(_))) {
Some(Msg::Transfer(TransferMsg::GoToParentDirectory)) Some(Msg::Transfer(TransferMsg::GoToParentDirectory))
} else { } else {
Some(Msg::Transfer(TransferMsg::EnterDirectory)) Some(Msg::Transfer(TransferMsg::EnterDirectory))
@@ -474,7 +502,7 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
code: Key::Char(' '), code: Key::Char(' '),
.. ..
}) => { }) => {
if matches!(self.component.state(), State::One(StateValue::String(_))) { if matches!(self.component.state(), State::Single(StateValue::String(_))) {
Some(Msg::None) Some(Msg::None)
} else { } else {
Some(Msg::Transfer(TransferMsg::TransferFile)) Some(Msg::Transfer(TransferMsg::TransferFile))
@@ -585,7 +613,7 @@ impl Component<Msg, NoUserEvent> for ExplorerLocal {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct ExplorerRemote { pub struct ExplorerRemote {
component: FileList, component: FileList,
} }
@@ -597,16 +625,25 @@ impl ExplorerRemote {
.background(bg) .background(bg)
.borders(Borders::default().color(hg)) .borders(Borders::default().color(hg))
.foreground(fg) .foreground(fg)
.highlighted_color(hg) .highlight_color(hg)
.title(title, Alignment::Left) .title(Title::from(title.as_ref().to_string()).alignment(HorizontalAlignment::Left))
.rows(files.iter().map(|x| vec![TextSpan::from(*x)]).collect()) .rows(
files
.iter()
.map(|x| {
vec![tuirealm::props::LineStatic::from(SpanStatic::from(
(*x).to_string(),
))]
})
.collect(),
)
.dot_dot(true), .dot_dot(true),
} }
} }
} }
impl Component<Msg, NoUserEvent> for ExplorerRemote { impl AppComponent<Msg, NoUserEvent> for ExplorerRemote {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Down, .. code: Key::Down, ..
@@ -659,7 +696,7 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
code: Key::Char('m'), code: Key::Char('m'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}) => { }) => {
let CmdResult::Changed(State::One(StateValue::Usize(index))) = let CmdResult::Changed(State::Single(StateValue::Usize(index))) =
self.perform(Cmd::Toggle) self.perform(Cmd::Toggle)
else { else {
return Some(Msg::None); return Some(Msg::None);
@@ -682,7 +719,7 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => { }) => {
if matches!(self.component.state(), State::One(StateValue::String(_))) { if matches!(self.component.state(), State::Single(StateValue::String(_))) {
Some(Msg::Transfer(TransferMsg::GoToParentDirectory)) Some(Msg::Transfer(TransferMsg::GoToParentDirectory))
} else { } else {
Some(Msg::Transfer(TransferMsg::EnterDirectory)) Some(Msg::Transfer(TransferMsg::EnterDirectory))
@@ -692,7 +729,7 @@ impl Component<Msg, NoUserEvent> for ExplorerRemote {
code: Key::Char(' '), code: Key::Char(' '),
.. ..
}) => { }) => {
if matches!(self.component.state(), State::One(StateValue::String(_))) { if matches!(self.component.state(), State::Single(StateValue::String(_))) {
Some(Msg::None) Some(Msg::None)
} else { } else {
Some(Msg::Transfer(TransferMsg::TransferFile)) Some(Msg::Transfer(TransferMsg::TransferFile))
@@ -3,12 +3,14 @@
//! `FileList` component renders a file list tab //! `FileList` component renders a file list tab
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::component::Component;
use tuirealm::props::{ use tuirealm::props::{
Alignment, AttrValue, Attribute, Borders, Color, Style, Table, TextModifiers, TextSpan, AttrValue, Attribute, Borders, Color, HorizontalAlignment, Props, QueryResult, SpanStatic,
Style, Table, TextModifiers, Title,
}; };
use tuirealm::ratatui::text::{Line, Span}; use tuirealm::ratatui::text::{Line, Span};
use tuirealm::ratatui::widgets::{List as TuiList, ListDirection, ListItem, ListState}; use tuirealm::ratatui::widgets::{List as TuiList, ListDirection, ListItem, ListState};
use tuirealm::{MockComponent, Props, State, StateValue}; use tuirealm::state::{State, StateValue};
pub const FILE_LIST_CMD_SELECT_ALL: &str = "A"; pub const FILE_LIST_CMD_SELECT_ALL: &str = "A";
pub const FILE_LIST_CMD_DESELECT_ALL: &str = "D"; pub const FILE_LIST_CMD_DESELECT_ALL: &str = "D";
@@ -108,16 +110,16 @@ impl FileList {
self self
} }
pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self { pub fn title(mut self, t: Title) -> Self {
self.attr( self.attr(Attribute::Title, AttrValue::Title(t));
Attribute::Title,
AttrValue::Title((t.as_ref().to_string(), a)),
);
self self
} }
pub fn highlighted_color(mut self, c: Color) -> Self { pub fn highlight_color(mut self, c: Color) -> Self {
self.attr(Attribute::HighlightedColor, AttrValue::Color(c)); self.attr(
Attribute::HighlightStyle,
AttrValue::Style(Style::default().fg(c).add_modifier(TextModifiers::REVERSED)),
);
self self
} }
@@ -136,40 +138,57 @@ impl FileList {
fn has_dot_dot(&self) -> bool { fn has_dot_dot(&self) -> bool {
self.props self.props
.get(Attribute::Custom(PROP_DOT_DOT)) .get(Attribute::Custom(PROP_DOT_DOT))
.map(AttrValue::unwrap_flag) .and_then(AttrValue::as_flag)
.unwrap_or(false) .unwrap_or(false)
} }
} }
impl MockComponent for FileList { impl Component for FileList {
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::ratatui::layout::Rect) { fn view(
&mut self,
frame: &mut tuirealm::ratatui::Frame,
area: tuirealm::ratatui::layout::Rect,
) {
let title = self let title = self
.props .props
.get_or( .get(Attribute::Title)
Attribute::Title, .and_then(AttrValue::as_title)
AttrValue::Title((String::default(), Alignment::Left)), .cloned()
) .unwrap_or_else(|| Title::from(String::default()).alignment(HorizontalAlignment::Left));
.unwrap_title();
let borders = self let borders = self
.props .props
.get_or(Attribute::Borders, AttrValue::Borders(Borders::default())) .get(Attribute::Borders)
.unwrap_borders(); .and_then(AttrValue::as_borders)
.unwrap_or_default();
let focus = self let focus = self
.props .props
.get_or(Attribute::Focus, AttrValue::Flag(false)) .get(Attribute::Focus)
.unwrap_flag(); .and_then(AttrValue::as_flag)
.unwrap_or(false);
let div = tui_realm_stdlib::utils::get_block(borders, Some(&title), focus, None); let div = tui_realm_stdlib::utils::get_block(borders, Some(&title), focus, None);
// Make list entries // Make list entries
let init_table_iter = if self.has_dot_dot() { let init_table_iter = if self.has_dot_dot() {
vec![vec![TextSpan::from("..")]] vec![vec![tuirealm::props::LineStatic::from(SpanStatic::from(
"..",
))]]
} else { } else {
vec![] vec![]
}; };
let foreground = self
.props
.get(Attribute::Foreground)
.and_then(AttrValue::as_color)
.unwrap_or(Color::Reset);
let background = self
.props
.get(Attribute::Background)
.and_then(AttrValue::as_color)
.unwrap_or(Color::Reset);
let list_items: Vec<ListItem> = match self let list_items: Vec<ListItem> = match self
.props .props
.get(Attribute::Content) .get(Attribute::Content)
.map(AttrValue::unwrap_table) .and_then(AttrValue::as_table)
{ {
Some(table) => init_table_iter Some(table) => init_table_iter
.iter() .iter()
@@ -177,39 +196,44 @@ impl MockComponent for FileList {
.map(|row| { .map(|row| {
let columns: Vec<Span> = row let columns: Vec<Span> = row
.iter() .iter()
.map(|col| { .flat_map(|line| line.spans.iter())
let (fg, bg, modifiers) = .map(|span| {
tui_realm_stdlib::utils::use_or_default_styles(&self.props, col); let mut style = span.style;
if style.fg.is_none() {
Span::styled( style.fg = Some(foreground);
col.content.clone(), }
Style::default().add_modifier(modifiers).fg(fg).bg(bg), if style.bg.is_none() {
) style.bg = Some(background);
}
Span::styled(span.content.clone(), style)
}) })
.collect(); .collect();
ListItem::new(Line::from(columns)) ListItem::new(Line::from(columns))
}) })
.collect(), // Make List item from TextSpan .collect(),
_ => Vec::new(), _ => Vec::new(),
}; };
let highlighted_color = self let highlight_style = self
.props .props
.get(Attribute::HighlightedColor) .get(Attribute::HighlightStyle)
.map(AttrValue::unwrap_color); .and_then(AttrValue::as_style);
let modifiers = match focus {
true => TextModifiers::REVERSED,
false => TextModifiers::empty(),
};
// Make list // Make list
let mut list = TuiList::new(list_items) let mut list = TuiList::new(list_items)
.block(div) .block(div)
.direction(ListDirection::TopToBottom); .direction(ListDirection::TopToBottom);
if let Some(highlighted_color) = highlighted_color { if let Some(style) = highlight_style {
list = list.highlight_style( let applied = if focus {
Style::default() style
.fg(highlighted_color) } else {
.add_modifier(modifiers), // On blur keep the highlight color on the foreground but drop the
); // REVERSED modifier so the selected row just stands out via fg only.
let mut s = Style::default();
if let Some(fg) = style.fg {
s = s.fg(fg);
}
s
};
list = list.highlight_style(applied);
} }
let mut state: ListState = ListState::default(); let mut state: ListState = ListState::default();
state.select(Some(self.states.list_index)); state.select(Some(self.states.list_index));
@@ -219,31 +243,27 @@ impl MockComponent for FileList {
fn attr(&mut self, attr: Attribute, value: AttrValue) { fn attr(&mut self, attr: Attribute, value: AttrValue) {
self.props.set(attr, value); self.props.set(attr, value);
if matches!(attr, Attribute::Content) { if matches!(attr, Attribute::Content) {
self.states.init_list_states( let len = self
match self .props
.props .get(Attribute::Content)
.get(Attribute::Content) .and_then(AttrValue::as_table)
.map(AttrValue::unwrap_table) .map(std::vec::Vec::len)
{ .unwrap_or(0);
Some(line) => line.len(), self.states.init_list_states(len, self.has_dot_dot());
_ => 0,
},
self.has_dot_dot(),
);
self.states.fix_list_index(); self.states.fix_list_index();
} }
} }
fn query(&self, attr: Attribute) -> Option<AttrValue> { fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
self.props.get(attr) self.props.get_for_query(attr)
} }
fn state(&self) -> State { fn state(&self) -> State {
if self.has_dot_dot() && self.states.list_index == 0 { if self.has_dot_dot() && self.states.list_index == 0 {
return State::One(StateValue::String("..".to_string())); return State::Single(StateValue::String("..".to_string()));
} }
State::One(StateValue::Usize(if self.has_dot_dot() { State::Single(StateValue::Usize(if self.has_dot_dot() {
self.states.list_index.saturating_sub(1) self.states.list_index.saturating_sub(1)
} else { } else {
self.states.list_index self.states.list_index
@@ -258,7 +278,7 @@ impl MockComponent for FileList {
if prev != self.states.list_index { if prev != self.states.list_index {
CmdResult::Changed(self.state()) CmdResult::Changed(self.state())
} else { } else {
CmdResult::None CmdResult::NoChange
} }
} }
Cmd::Move(Direction::Up) => { Cmd::Move(Direction::Up) => {
@@ -267,7 +287,7 @@ impl MockComponent for FileList {
if prev != self.states.list_index { if prev != self.states.list_index {
CmdResult::Changed(self.state()) CmdResult::Changed(self.state())
} else { } else {
CmdResult::None CmdResult::NoChange
} }
} }
Cmd::Scroll(Direction::Down) => { Cmd::Scroll(Direction::Down) => {
@@ -276,7 +296,7 @@ impl MockComponent for FileList {
if prev != self.states.list_index { if prev != self.states.list_index {
CmdResult::Changed(self.state()) CmdResult::Changed(self.state())
} else { } else {
CmdResult::None CmdResult::NoChange
} }
} }
Cmd::Scroll(Direction::Up) => { Cmd::Scroll(Direction::Up) => {
@@ -285,7 +305,7 @@ impl MockComponent for FileList {
if prev != self.states.list_index { if prev != self.states.list_index {
CmdResult::Changed(self.state()) CmdResult::Changed(self.state())
} else { } else {
CmdResult::None CmdResult::NoChange
} }
} }
Cmd::GoTo(Position::Begin) => { Cmd::GoTo(Position::Begin) => {
@@ -294,7 +314,7 @@ impl MockComponent for FileList {
if prev != self.states.list_index { if prev != self.states.list_index {
CmdResult::Changed(self.state()) CmdResult::Changed(self.state())
} else { } else {
CmdResult::None CmdResult::NoChange
} }
} }
Cmd::GoTo(Position::End) => { Cmd::GoTo(Position::End) => {
@@ -303,12 +323,12 @@ impl MockComponent for FileList {
if prev != self.states.list_index { if prev != self.states.list_index {
CmdResult::Changed(self.state()) CmdResult::Changed(self.state())
} else { } else {
CmdResult::None CmdResult::NoChange
} }
} }
Cmd::Toggle => { Cmd::Toggle => {
if self.states.list_index == 0 && self.has_dot_dot() { if self.states.list_index == 0 && self.has_dot_dot() {
return CmdResult::None; return CmdResult::NoChange;
} }
let index = self.states.real_index(); let index = self.states.real_index();
@@ -317,9 +337,9 @@ impl MockComponent for FileList {
.list_index .list_index
.saturating_add(1) .saturating_add(1)
.min(self.states.list_len.saturating_sub(1)); .min(self.states.list_len.saturating_sub(1));
CmdResult::Changed(State::One(StateValue::Usize(index))) CmdResult::Changed(State::Single(StateValue::Usize(index)))
} }
_ => CmdResult::None, _ => CmdResult::NoChange,
} }
} }
} }
@@ -1,8 +1,12 @@
use tui_realm_stdlib::Input; use tui_realm_stdlib::components::Input;
use tuirealm::command::{Cmd, CmdResult}; use tuirealm::command::{Cmd, CmdResult};
use tuirealm::props::{Alignment, AttrValue, Attribute, Borders, Color, Table}; use tuirealm::component::Component;
use tuirealm::props::{
AttrValue, Attribute, Borders, Color, HorizontalAlignment, QueryResult, Style, Table,
TextModifiers, Title,
};
use tuirealm::ratatui::layout::{Constraint, Direction, Layout}; use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
use tuirealm::{MockComponent, State}; use tuirealm::state::State;
use super::file_list::FileList; use super::file_list::FileList;
@@ -62,21 +66,21 @@ impl FileListWithSearch {
self self
} }
pub fn title<S: AsRef<str>>(mut self, t: S, a: Alignment) -> Self { pub fn title(mut self, t: Title) -> Self {
self.file_list.attr( let align = t.content.alignment.unwrap_or(HorizontalAlignment::Left);
Attribute::Title, self.file_list.attr(Attribute::Title, AttrValue::Title(t));
AttrValue::Title((t.as_ref().to_string(), a)),
);
self.search.attr( self.search.attr(
Attribute::Title, Attribute::Title,
AttrValue::Title(("Fuzzy search".to_string(), a)), AttrValue::Title(Title::from("Fuzzy search".to_string()).alignment(align)),
); );
self self
} }
pub fn highlighted_color(mut self, c: Color) -> Self { pub fn highlight_color(mut self, c: Color) -> Self {
self.file_list self.file_list.attr(
.attr(Attribute::HighlightedColor, AttrValue::Color(c)); Attribute::HighlightStyle,
AttrValue::Style(Style::default().fg(c).add_modifier(TextModifiers::REVERSED)),
);
self self
} }
@@ -87,8 +91,12 @@ impl FileListWithSearch {
} }
} }
impl MockComponent for FileListWithSearch { impl Component for FileListWithSearch {
fn view(&mut self, frame: &mut tuirealm::Frame, area: tuirealm::ratatui::layout::Rect) { fn view(
&mut self,
frame: &mut tuirealm::ratatui::Frame,
area: tuirealm::ratatui::layout::Rect,
) {
// split the area in two // split the area in two
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
@@ -107,7 +115,7 @@ impl MockComponent for FileListWithSearch {
self.file_list.view(frame, chunks[1]); self.file_list.view(frame, chunks[1]);
} }
fn query(&self, attr: Attribute) -> Option<AttrValue> { fn query<'a>(&'a self, attr: Attribute) -> Option<QueryResult<'a>> {
self.file_list.query(attr) self.file_list.query(attr)
} }
@@ -151,7 +159,7 @@ impl MockComponent for FileListWithSearch {
AttrValue::Flag(self.states.focus == Focus::List), AttrValue::Flag(self.states.focus == Focus::List),
); );
CmdResult::None CmdResult::NoChange
} }
cmd if self.states.focus == Focus::Search => self.search.perform(cmd), cmd if self.states.focus == Focus::Search => self.search.perform(cmd),
cmd => self.file_list.perform(cmd), cmd => self.file_list.perform(cmd),
+5 -2
View File
@@ -3,14 +3,17 @@ mod host;
mod log; mod log;
mod notify; mod notify;
use tuirealm::{PollStrategy, Update}; use tuirealm::application::PollStrategy;
use super::FileTransferActivity; use super::FileTransferActivity;
impl FileTransferActivity { impl FileTransferActivity {
/// Call `Application::tick()` and process messages in `Update` /// Call `Application::tick()` and process messages in `Update`
pub(super) fn tick(&mut self) { pub(super) fn tick(&mut self) {
match self.app.tick(PollStrategy::UpTo(1)) { match self
.app
.tick(PollStrategy::UpTo(1, std::time::Duration::from_millis(10)))
{
Ok(messages) => { Ok(messages) => {
if !messages.is_empty() { if !messages.is_empty() {
self.redraw = true; self.redraw = true;
+25 -20
View File
@@ -1,6 +1,8 @@
use tuirealm::props::{ use tuirealm::props::{
Alignment, AttrValue, Attribute, PropPayload, PropValue, TextModifiers, TextSpan, AttrValue, Attribute, HorizontalAlignment, LineStatic, PropPayload, PropValue, SpanStatic,
TextModifiers, Title,
}; };
use tuirealm::terminal::TerminalAdapter;
use super::super::browser::FileExplorerTab; use super::super::browser::FileExplorerTab;
use super::super::{FileTransferActivity, Id, ui_result}; use super::super::{FileTransferActivity, Id, ui_result};
@@ -33,17 +35,18 @@ impl FileTransferActivity {
hostname.len() + 3 hostname.len() + 3
) // 3 because of '/…/' ) // 3 because of '/…/'
); );
let files: Vec<Vec<TextSpan>> = self let files: Vec<Vec<LineStatic>> = self
.host_bridge() .host_bridge()
.iter_files() .iter_files()
.map(|x| { .map(|x| {
let mut span = TextSpan::from(self.host_bridge().fmt_file(x)); let mut span = SpanStatic::from(self.host_bridge().fmt_file(x));
if self.host_bridge().enqueued().contains_key(x.path()) { if self.host_bridge().enqueued().contains_key(x.path()) {
span.modifiers |= span.style = span.style.add_modifier(
TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC; TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC,
);
} }
vec![span] vec![LineStatic::from(span)]
}) })
.collect(); .collect();
// Update content and title // Update content and title
@@ -55,7 +58,7 @@ impl FileTransferActivity {
ui_result(self.app.attr( ui_result(self.app.attr(
&Id::ExplorerHostBridge, &Id::ExplorerHostBridge,
Attribute::Title, Attribute::Title,
AttrValue::Title((hostname, Alignment::Left)), AttrValue::Title(Title::from(hostname).alignment(HorizontalAlignment::Left)),
)); ));
} }
@@ -147,17 +150,18 @@ impl FileTransferActivity {
hostname.len() + 3 // 3 because of '/…/' hostname.len() + 3 // 3 because of '/…/'
) )
); );
let files: Vec<Vec<TextSpan>> = self let files: Vec<Vec<LineStatic>> = self
.remote() .remote()
.iter_files() .iter_files()
.map(|x| { .map(|x| {
let mut span = TextSpan::from(self.remote().fmt_file(x)); let mut span = SpanStatic::from(self.remote().fmt_file(x));
if self.remote().enqueued().contains_key(x.path()) { if self.remote().enqueued().contains_key(x.path()) {
span.modifiers |= span.style = span.style.add_modifier(
TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC; TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC,
);
} }
vec![span] vec![LineStatic::from(span)]
}) })
.collect(); .collect();
// Update content and title // Update content and title
@@ -169,7 +173,7 @@ impl FileTransferActivity {
ui_result(self.app.attr( ui_result(self.app.attr(
&Id::ExplorerRemote, &Id::ExplorerRemote,
Attribute::Title, Attribute::Title,
AttrValue::Title((hostname, Alignment::Left)), AttrValue::Title(Title::from(hostname).alignment(HorizontalAlignment::Left)),
)); ));
} }
@@ -185,7 +189,7 @@ impl FileTransferActivity {
ui_result(self.app.attr( ui_result(self.app.attr(
&Id::TransferProgressBar, &Id::TransferProgressBar,
Attribute::Value, Attribute::Value,
AttrValue::Payload(PropPayload::One(PropValue::F64( AttrValue::Payload(PropPayload::Single(PropValue::F64(
self.transfer.progress.calc_progress(), self.transfer.progress.calc_progress(),
))), ))),
)); ));
@@ -201,7 +205,7 @@ impl FileTransferActivity {
ui_result(self.app.attr( ui_result(self.app.attr(
&Id::TransferProgressBar, &Id::TransferProgressBar,
Attribute::Title, Attribute::Title,
AttrValue::Title((title, Alignment::Center)), AttrValue::Title(Title::from(title).alignment(HorizontalAlignment::Center)),
)); ));
} }
@@ -231,17 +235,18 @@ impl FileTransferActivity {
} }
pub(in crate::ui::activities::filetransfer) fn update_find_list(&mut self) { pub(in crate::ui::activities::filetransfer) fn update_find_list(&mut self) {
let files: Vec<Vec<TextSpan>> = self let files: Vec<Vec<LineStatic>> = self
.found() .found()
.unwrap() .unwrap()
.iter_files() .iter_files()
.map(|x| { .map(|x| {
let mut span = TextSpan::from(self.found().unwrap().fmt_file(x)); let mut span = SpanStatic::from(self.found().unwrap().fmt_file(x));
if self.found().unwrap().enqueued().contains_key(x.path()) { if self.found().unwrap().enqueued().contains_key(x.path()) {
span.modifiers |= span.style = span.style.add_modifier(
TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC; TextModifiers::REVERSED | TextModifiers::UNDERLINED | TextModifiers::ITALIC,
);
} }
vec![span] vec![LineStatic::from(span)]
}) })
.collect(); .collect();
ui_result(self.app.attr( ui_result(self.app.attr(
+12 -19
View File
@@ -1,4 +1,5 @@
use tuirealm::props::{AttrValue, Attribute, Color, TableBuilder, TextSpan}; use tuirealm::props::{AttrValue, Attribute, Color, SpanStatic, TableBuilder};
use tuirealm::ratatui::style::Stylize;
use super::super::{FileTransferActivity, Id, LogLevel, LogRecord, ui_result}; use super::super::{FileTransferActivity, Id, LogLevel, LogRecord, ui_result};
@@ -52,28 +53,20 @@ impl FileTransferActivity {
LogLevel::Warn => Color::Yellow, LogLevel::Warn => Color::Yellow,
LogLevel::Info => Color::Green, LogLevel::Info => Color::Green,
}; };
let level_label = match record.level {
LogLevel::Error => "ERROR",
LogLevel::Warn => "WARN",
LogLevel::Info => "INFO",
};
table table
.add_col(TextSpan::from(format!( .add_col(SpanStatic::from(format!(
"{}", "{}",
record.time.format("%Y-%m-%dT%H:%M:%S%Z") record.time.format("%Y-%m-%dT%H:%M:%S%Z")
))) )))
.add_col(TextSpan::from(" [")) .add_col(SpanStatic::from(" ["))
.add_col( .add_col(SpanStatic::raw(format!("{level_label:5}")).fg(fg))
TextSpan::new( .add_col(SpanStatic::from("]: "))
format!( .add_col(SpanStatic::from(record.msg.clone()));
"{:5}",
match record.level {
LogLevel::Error => "ERROR",
LogLevel::Warn => "WARN",
LogLevel::Info => "INFO",
}
)
.as_str(),
)
.fg(fg),
)
.add_col(TextSpan::from("]: "))
.add_col(TextSpan::from(record.msg.as_str()));
} }
ui_result(self.app.attr( ui_result(self.app.attr(
&Id::Log, &Id::Log,
+4 -4
View File
@@ -5,7 +5,7 @@
// locals // locals
// externals // externals
use remotefs::fs::File; use remotefs::fs::File;
use tuirealm::{State, StateValue, Update}; use tuirealm::state::{State, StateValue};
use super::actions::SelectedFile; use super::actions::SelectedFile;
use super::actions::walkdir::WalkdirError; use super::actions::walkdir::WalkdirError;
@@ -15,8 +15,8 @@ use super::{
ui_result, ui_result,
}; };
impl Update<Msg> for FileTransferActivity { impl FileTransferActivity {
fn update(&mut self, msg: Option<Msg>) -> Option<Msg> { pub(super) fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
match msg.unwrap_or(Msg::None) { match msg.unwrap_or(Msg::None) {
Msg::None => None, Msg::None => None,
Msg::PendingAction(_) => { Msg::PendingAction(_) => {
@@ -78,7 +78,7 @@ impl FileTransferActivity {
self.action_find_delete(); self.action_find_delete();
// Remove deleted entries from the find-result list // Remove deleted entries from the find-result list
match self.app.state(&Id::ExplorerFind) { match self.app.state(&Id::ExplorerFind) {
Ok(State::One(StateValue::Usize(idx))) => { Ok(State::Single(StateValue::Usize(idx))) => {
self.found_mut().unwrap().del_entry(idx); self.found_mut().unwrap().del_entry(idx);
} }
Ok(State::Vec(values)) => { Ok(State::Vec(values)) => {
+25 -20
View File
@@ -3,9 +3,11 @@
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall //! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::props::Attribute;
use tuirealm::ratatui::layout::{Constraint, Direction, Layout}; use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
use tuirealm::ratatui::widgets::Clear; use tuirealm::ratatui::widgets::Clear;
use tuirealm::{Attribute, Sub, SubClause, SubEventClause}; use tuirealm::subscription::{EventClause, Sub, SubClause};
use tuirealm::terminal::TerminalAdapter;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use crate::ui::activities::filetransfer::browser::FoundExplorerTab; use crate::ui::activities::filetransfer::browser::FoundExplorerTab;
@@ -215,7 +217,12 @@ impl FileTransferActivity {
let lines = self let lines = self
.app .app
.query(&Id::WaitPopup, Attribute::Text) .query(&Id::WaitPopup, Attribute::Text)
.map(|x| x.map(|x| x.unwrap_payload().unwrap_vec().len())) .map(|x| {
x.map(|x| match x.into_attr() {
tuirealm::props::AttrValue::Text(t) => t.lines.len(),
_ => 1,
})
})
.unwrap_or_default() .unwrap_or_default()
.unwrap_or(1) as u16; .unwrap_or(1) as u16;
let popup = let popup =
@@ -270,21 +277,19 @@ impl FileTransferActivity {
// Get current text width // Get current text width
let text_width = self let text_width = self
.app .app
.query(&id, tuirealm::Attribute::Text) .query(&id, tuirealm::props::Attribute::Text)
.ok() .ok()
.flatten() .flatten()
.map(|x| { .map(|x| match x.into_attr() {
if x.as_payload().is_none() { tuirealm::props::AttrValue::Text(t) => t
return 0; .lines
} .iter()
.flat_map(|line| line.spans.iter())
x.unwrap_payload() .map(|span| span.content.as_ref())
.unwrap_vec() .collect::<Vec<&str>>()
.into_iter()
.map(|x| x.unwrap_text_span().content)
.collect::<Vec<String>>()
.join("") .join("")
.width() as u16 .width() as u16,
_ => 0,
}) })
.unwrap_or(0); .unwrap_or(0);
// Calc real width of a row in the popup // Calc real width of a row in the popup
@@ -305,41 +310,41 @@ impl FileTransferActivity {
Box::<components::GlobalListener>::default(), Box::<components::GlobalListener>::default(),
vec![ vec![
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Esc, code: Key::Esc,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Char('h'), code: Key::Char('h'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Function(1), code: Key::Function(1),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Function(10), code: Key::Function(10),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Char('q'), code: Key::Char('q'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new(SubEventClause::WindowResize, SubClause::Always), Sub::new(EventClause::WindowResize, SubClause::Always),
], ],
)); ));
} }
@@ -3,8 +3,7 @@
//! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall //! `filetransfer_activity` is the module which implements the Filetransfer activity, which is the main activity afterall
use remotefs::fs::{File, UnixPex}; use remotefs::fs::{File, UnixPex};
use tuirealm::props::{PropPayload, PropValue, TextSpan}; use tuirealm::props::{AttrValue, Attribute, PropPayload, PropValue, SpanStatic};
use tuirealm::{AttrValue, Attribute};
use crate::explorer::FileSorting; use crate::explorer::FileSorting;
use crate::ui::activities::filetransfer::browser::FileExplorerTab; use crate::ui::activities::filetransfer::browser::FileExplorerTab;
@@ -93,9 +92,9 @@ impl FileTransferActivity {
let _ = self.app.attr( let _ = self.app.attr(
&Id::WaitPopup, &Id::WaitPopup,
Attribute::Text, Attribute::Text,
AttrValue::Payload(PropPayload::Vec(vec![ AttrValue::Text(tuirealm::ratatui::text::Text::from_iter([
PropValue::TextSpan(TextSpan::from(text)), SpanStatic::from(text),
PropValue::TextSpan(TextSpan::from("Press 'CTRL+C' to abort")), SpanStatic::from("Press 'CTRL+C' to abort"),
])), ])),
); );
+8 -6
View File
@@ -14,10 +14,11 @@ mod view;
// Ext // Ext
use std::time::Duration; use std::time::Duration;
use tuirealm::application::PollStrategy; use tuirealm::application::{Application, PollStrategy};
use tuirealm::event::NoUserEvent;
use tuirealm::listener::EventListenerCfg; use tuirealm::listener::EventListenerCfg;
use tuirealm::props::Color; use tuirealm::props::Color;
use tuirealm::{Application, NoUserEvent, Update}; use tuirealm::terminal::TerminalAdapter;
use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason}; use super::{Activity, CROSSTERM_MAX_POLL, Context, ExitReason};
use crate::config::themes::Theme; use crate::config::themes::Theme;
@@ -258,9 +259,7 @@ impl SetupActivity {
pub fn new(ticks: Duration) -> Self { pub fn new(ticks: Duration) -> Self {
Self { Self {
app: Application::init( app: Application::init(
EventListenerCfg::default() EventListenerCfg::default().crossterm_input_listener(ticks, CROSSTERM_MAX_POLL),
.crossterm_input_listener(ticks, CROSSTERM_MAX_POLL)
.poll_timeout(ticks),
), ),
exit_reason: None, exit_reason: None,
context: None, context: None,
@@ -347,7 +346,10 @@ impl Activity for SetupActivity {
if self.context.is_none() { if self.context.is_none() {
return; return;
} }
match self.app.tick(PollStrategy::UpTo(3)) { match self
.app
.tick(PollStrategy::UpTo(3, std::time::Duration::from_millis(10)))
{
Ok(messages) => { Ok(messages) => {
if !messages.is_empty() { if !messages.is_empty() {
self.redraw = true; self.redraw = true;
+6 -5
View File
@@ -7,7 +7,8 @@
use std::env; use std::env;
use tuirealm::ratatui::style::Color; use tuirealm::ratatui::style::Color;
use tuirealm::{State, StateValue}; use tuirealm::state::{State, StateValue};
use tuirealm::terminal::TerminalAdapter;
use super::{Id, IdSsh, IdTheme, SetupActivity, ViewLayout}; use super::{Id, IdSsh, IdTheme, SetupActivity, ViewLayout};
// Ext // Ext
@@ -96,7 +97,7 @@ impl SetupActivity {
// Get key // Get key
// get index // get index
let idx: Option<usize> = match self.app.state(&Id::Ssh(IdSsh::SshKeys)) { let idx: Option<usize> = match self.app.state(&Id::Ssh(IdSsh::SshKeys)) {
Ok(State::One(StateValue::Usize(idx))) => Some(idx), Ok(State::Single(StateValue::Usize(idx))) => Some(idx),
_ => None, _ => None,
}; };
// get ssh key and delete it // get ssh key and delete it
@@ -114,11 +115,11 @@ impl SetupActivity {
pub(super) fn action_new_ssh_key(&mut self) -> Result<(), String> { pub(super) fn action_new_ssh_key(&mut self) -> Result<(), String> {
// get parameters // get parameters
let host: String = match self.app.state(&Id::Ssh(IdSsh::SshHost)) { let host: String = match self.app.state(&Id::Ssh(IdSsh::SshHost)) {
Ok(State::One(StateValue::String(host))) => host, Ok(State::Single(StateValue::String(host))) => host,
_ => String::new(), _ => String::new(),
}; };
let username: String = match self.app.state(&Id::Ssh(IdSsh::SshUsername)) { let username: String = match self.app.state(&Id::Ssh(IdSsh::SshUsername)) {
Ok(State::One(StateValue::String(user))) => user, Ok(State::Single(StateValue::String(user))) => user,
_ => String::new(), _ => String::new(),
}; };
// Prepare text editor // Prepare text editor
@@ -382,7 +383,7 @@ impl SetupActivity {
/// Get color from component /// Get color from component
fn get_color(&self, component: &Id) -> Result<Color, ()> { fn get_color(&self, component: &Id) -> Result<Color, ()> {
match self.app.state(component) { match self.app.state(component) {
Ok(State::One(StateValue::String(color))) => { Ok(State::Single(StateValue::String(color))) => {
match crate::utils::parser::parse_color(color.as_str()) { match crate::utils::parser::parse_color(color.as_str()) {
Some(c) => Ok(c), Some(c) => Ok(c),
None => Err(()), None => Err(()),
+5 -5
View File
@@ -16,19 +16,19 @@ pub(super) use config::{
}; };
pub(super) use ssh::{DelSshKeyPopup, SshHost, SshKeys, SshUsername}; pub(super) use ssh::{DelSshKeyPopup, SshHost, SshKeys, SshUsername};
pub(super) use theme::*; pub(super) use theme::*;
use tui_realm_stdlib::Phantom; use tui_realm_stdlib::components::Phantom;
use tuirealm::component::{AppComponent, Component};
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, MockComponent};
// -- global listener // -- global listener
#[derive(Default, MockComponent)] #[derive(Default, Component)]
pub struct GlobalListener { pub struct GlobalListener {
component: Phantom, component: Phantom,
} }
impl Component<Msg, NoUserEvent> for GlobalListener { impl AppComponent<Msg, NoUserEvent> for GlobalListener {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Function(10), code: Key::Esc | Key::Function(10),
+87 -65
View File
@@ -2,15 +2,21 @@
//! //!
//! config tab components //! config tab components
use tui_realm_stdlib::{List, Paragraph, Radio, Span}; use tui_realm_stdlib::components::{List, Paragraph, Radio, Span};
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderSides, BorderType, Borders, Color, TableBuilder, TextSpan}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::props::{
BorderSides, BorderType, Borders, Color, HorizontalAlignment, SpanStatic, Style, TableBuilder,
TextModifiers, Title,
};
use tuirealm::ratatui::style::Stylize;
use tuirealm::ratatui::text::Text;
use tuirealm::state::{State, StateValue};
use super::{CommonMsg, Msg, ViewLayout}; use super::{CommonMsg, Msg, ViewLayout};
#[derive(MockComponent)] #[derive(Component)]
pub struct ErrorPopup { pub struct ErrorPopup {
component: Paragraph, component: Paragraph,
} }
@@ -19,21 +25,23 @@ impl ErrorPopup {
pub fn new<S: AsRef<str>>(text: S) -> Self { pub fn new<S: AsRef<str>>(text: S) -> Self {
Self { Self {
component: Paragraph::default() component: Paragraph::default()
.alignment(Alignment::Center) .alignment_horizontal(HorizontalAlignment::Center)
.borders( .borders(
Borders::default() Borders::default()
.color(Color::Red) .color(Color::Red)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(Color::Red) .foreground(Color::Red)
.text([TextSpan::from(text.as_ref())]) .text(Text::from_iter([SpanStatic::from(
.wrap(true), text.as_ref().to_string(),
)]))
.wrap_trim(true),
} }
} }
} }
impl Component<Msg, NoUserEvent> for ErrorPopup { impl AppComponent<Msg, NoUserEvent> for ErrorPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter, code: Key::Esc | Key::Enter,
@@ -44,7 +52,7 @@ impl Component<Msg, NoUserEvent> for ErrorPopup {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct Footer { pub struct Footer {
component: Span, component: Span,
} }
@@ -53,28 +61,28 @@ impl Default for Footer {
fn default() -> Self { fn default() -> Self {
Self { Self {
component: Span::default().spans([ component: Span::default().spans([
TextSpan::new("<F1|CTRL+H>").bold().fg(Color::Cyan), SpanStatic::raw("<F1|CTRL+H>").bold().fg(Color::Cyan),
TextSpan::new(" Help "), SpanStatic::raw(" Help "),
TextSpan::new("<F4|CTRL+S>").bold().fg(Color::Cyan), SpanStatic::raw("<F4|CTRL+S>").bold().fg(Color::Cyan),
TextSpan::new(" Save parameters "), SpanStatic::raw(" Save parameters "),
TextSpan::new("<F10|ESC>").bold().fg(Color::Cyan), SpanStatic::raw("<F10|ESC>").bold().fg(Color::Cyan),
TextSpan::new(" Exit "), SpanStatic::raw(" Exit "),
TextSpan::new("<TAB>").bold().fg(Color::Cyan), SpanStatic::raw("<TAB>").bold().fg(Color::Cyan),
TextSpan::new(" Change panel "), SpanStatic::raw(" Change panel "),
TextSpan::new("<UP/DOWN>").bold().fg(Color::Cyan), SpanStatic::raw("<UP/DOWN>").bold().fg(Color::Cyan),
TextSpan::new(" Change field "), SpanStatic::raw(" Change field "),
]), ]),
} }
} }
} }
impl Component<Msg, NoUserEvent> for Footer { impl AppComponent<Msg, NoUserEvent> for Footer {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct Header { pub struct Header {
component: Radio, component: Radio,
} }
@@ -83,13 +91,17 @@ impl Header {
pub fn new(layout: ViewLayout) -> Self { pub fn new(layout: ViewLayout) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(Color::Yellow) .color(Color::Yellow)
.sides(BorderSides::BOTTOM), .sides(BorderSides::BOTTOM),
) )
.choices(["Configuration parameters", "SSH Keys", "Theme"]) .choices(["Configuration parameters", "SSH Keys", "Theme"])
.foreground(Color::Yellow)
.value(match layout { .value(match layout {
ViewLayout::SetupForm => 0, ViewLayout::SetupForm => 0,
ViewLayout::SshKeys => 1, ViewLayout::SshKeys => 1,
@@ -99,13 +111,13 @@ impl Header {
} }
} }
impl Component<Msg, NoUserEvent> for Header { impl AppComponent<Msg, NoUserEvent> for Header {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct Keybindings { pub struct Keybindings {
component: List, component: List,
} }
@@ -115,45 +127,47 @@ impl Default for Keybindings {
Self { Self {
component: List::default() component: List::default()
.borders(Borders::default().modifiers(BorderType::Rounded)) .borders(Borders::default().modifiers(BorderType::Rounded))
.title("Keybindings", Alignment::Center) .title(Title::from("Keybindings").alignment(HorizontalAlignment::Center))
.scroll(true) .scroll(true)
.highlighted_str("? ") .highlight_str("? ")
.rows( .rows(
TableBuilder::default() TableBuilder::default()
.add_col(TextSpan::new("<ESC>").bold().fg(Color::Cyan)) .add_col(SpanStatic::raw("<ESC>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Exit setup")) .add_col(SpanStatic::from(" Exit setup"))
.add_row() .add_row()
.add_col(TextSpan::new("<TAB>").bold().fg(Color::Cyan)) .add_col(SpanStatic::raw("<TAB>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change setup page")) .add_col(SpanStatic::from(" Change setup page"))
.add_row() .add_row()
.add_col(TextSpan::new("<RIGHT/LEFT>").bold().fg(Color::Cyan)) .add_col(SpanStatic::raw("<RIGHT/LEFT>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change cursor")) .add_col(SpanStatic::from(" Change cursor"))
.add_row() .add_row()
.add_col(TextSpan::new("<UP/DOWN>").bold().fg(Color::Cyan)) .add_col(SpanStatic::raw("<UP/DOWN>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Change input field")) .add_col(SpanStatic::from(" Change input field"))
.add_row() .add_row()
.add_col(TextSpan::new("<ENTER>").bold().fg(Color::Cyan)) .add_col(SpanStatic::raw("<ENTER>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Select / Dismiss popup")) .add_col(SpanStatic::from(" Select / Dismiss popup"))
.add_row() .add_row()
.add_col(TextSpan::new("<DEL|E>").bold().fg(Color::Cyan)) .add_col(SpanStatic::raw("<DEL|E>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Delete SSH key")) .add_col(SpanStatic::from(" Delete SSH key"))
.add_row() .add_row()
.add_col(TextSpan::new("<CTRL+N>").bold().fg(Color::Cyan)) .add_col(SpanStatic::raw("<CTRL+N>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" New SSH key")) .add_col(SpanStatic::from(" New SSH key"))
.add_row() .add_row()
.add_col(TextSpan::new("<CTRL+R>").bold().fg(Color::Cyan)) .add_col(SpanStatic::raw("<CTRL+R>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Revert changes")) .add_col(SpanStatic::from(" Revert changes"))
.add_row() .add_row()
.add_col(TextSpan::new("<CTRL+S>").bold().fg(Color::Cyan)) .add_col(SpanStatic::raw("<CTRL+S>").bold().fg(Color::Cyan))
.add_col(TextSpan::from(" Save configuration")) .add_col(SpanStatic::from(" Save configuration"))
.build(), .build()
.into_iter()
.map(|row| row.into_iter().flat_map(|l| l.spans).collect::<Vec<_>>()),
), ),
} }
} }
} }
impl Component<Msg, NoUserEvent> for Keybindings { impl AppComponent<Msg, NoUserEvent> for Keybindings {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Esc | Key::Enter, code: Key::Esc | Key::Enter,
@@ -197,7 +211,7 @@ impl Component<Msg, NoUserEvent> for Keybindings {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct QuitPopup { pub struct QuitPopup {
component: Radio, component: Radio,
} }
@@ -206,15 +220,19 @@ impl Default for QuitPopup {
fn default() -> Self { fn default() -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(Color::Red)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(Color::Red) .color(Color::Red)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(Color::Red)
.title( .title(
"There are unsaved changes! Save changes before leaving?", Title::from("There are unsaved changes! Save changes before leaving?")
Alignment::Center, .alignment(HorizontalAlignment::Center),
) )
.rewind(true) .rewind(true)
.choices(["Save", "Don't save", "Cancel"]), .choices(["Save", "Don't save", "Cancel"]),
@@ -222,8 +240,8 @@ impl Default for QuitPopup {
} }
} }
impl Component<Msg, NoUserEvent> for QuitPopup { impl AppComponent<Msg, NoUserEvent> for QuitPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Common(CommonMsg::CloseQuitPopup)) Some(Msg::Common(CommonMsg::CloseQuitPopup))
@@ -243,10 +261,10 @@ impl Component<Msg, NoUserEvent> for QuitPopup {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.perform(Cmd::Submit) { }) => match self.perform(Cmd::Submit) {
CmdResult::Submit(State::One(StateValue::Usize(0))) => { CmdResult::Submit(State::Single(StateValue::Usize(0))) => {
Some(Msg::Common(CommonMsg::SaveAndQuit)) Some(Msg::Common(CommonMsg::SaveAndQuit))
} }
CmdResult::Submit(State::One(StateValue::Usize(1))) => { CmdResult::Submit(State::Single(StateValue::Usize(1))) => {
Some(Msg::Common(CommonMsg::Quit)) Some(Msg::Common(CommonMsg::Quit))
} }
_ => Some(Msg::Common(CommonMsg::CloseQuitPopup)), _ => Some(Msg::Common(CommonMsg::CloseQuitPopup)),
@@ -256,7 +274,7 @@ impl Component<Msg, NoUserEvent> for QuitPopup {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct SavePopup { pub struct SavePopup {
component: Radio, component: Radio,
} }
@@ -265,21 +283,25 @@ impl Default for SavePopup {
fn default() -> Self { fn default() -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(Color::Yellow)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(Color::Yellow) .color(Color::Yellow)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(Color::Yellow) .title(Title::from("Save changes?").alignment(HorizontalAlignment::Center))
.title("Save changes?", Alignment::Center)
.rewind(true) .rewind(true)
.choices(["Yes", "No"]), .choices(["Yes", "No"]),
} }
} }
} }
impl Component<Msg, NoUserEvent> for SavePopup { impl AppComponent<Msg, NoUserEvent> for SavePopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Common(CommonMsg::CloseSavePopup)) Some(Msg::Common(CommonMsg::CloseSavePopup))
@@ -309,7 +331,7 @@ impl Component<Msg, NoUserEvent> for SavePopup {
}) => { }) => {
if matches!( if matches!(
self.perform(Cmd::Submit), self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0))) CmdResult::Submit(State::Single(StateValue::Usize(0)))
) { ) {
Some(Msg::Common(CommonMsg::SaveConfig)) Some(Msg::Common(CommonMsg::SaveConfig))
} else { } else {
+97 -62
View File
@@ -2,11 +2,13 @@
//! //!
//! config tab components //! config tab components
use tui_realm_stdlib::{Input, Radio}; use tui_realm_stdlib::components::{Input, Radio};
use tuirealm::command::{Cmd, Direction, Position}; use tuirealm::command::{Cmd, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{Component, Event, MockComponent, NoUserEvent}; use tuirealm::props::{
BorderType, Borders, Color, HorizontalAlignment, InputType, Style, TextModifiers, Title,
};
use super::{ConfigMsg, Msg}; use super::{ConfigMsg, Msg};
use crate::explorer::GroupDirs as GroupDirsEnum; use crate::explorer::GroupDirs as GroupDirsEnum;
@@ -19,7 +21,7 @@ use crate::utils::parser::parse_bytesize;
// -- components // -- components
#[derive(MockComponent)] #[derive(Component)]
pub struct CheckUpdates { pub struct CheckUpdates {
component: Radio, component: Radio,
} }
@@ -28,22 +30,26 @@ impl CheckUpdates {
pub fn new(enabled: bool) -> Self { pub fn new(enabled: bool) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(Color::LightYellow)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(Color::LightYellow) .color(Color::LightYellow)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.choices(["Yes", "No"]) .choices(["Yes", "No"])
.foreground(Color::LightYellow)
.rewind(true) .rewind(true)
.title("Check for updates?", Alignment::Left) .title(Title::from("Check for updates?").alignment(HorizontalAlignment::Left))
.value(usize::from(!enabled)), .value(usize::from(!enabled)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for CheckUpdates { impl AppComponent<Msg, NoUserEvent> for CheckUpdates {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev( handle_radio_ev(
self, self,
ev, ev,
@@ -53,7 +59,7 @@ impl Component<Msg, NoUserEvent> for CheckUpdates {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct DefaultProtocol { pub struct DefaultProtocol {
component: Radio, component: Radio,
} }
@@ -70,7 +76,7 @@ impl DefaultProtocol {
.choices(["SFTP", "SCP", "FTP", "FTPS", "Kube", "S3", "SMB", "WebDAV"]) .choices(["SFTP", "SCP", "FTP", "FTPS", "Kube", "S3", "SMB", "WebDAV"])
.foreground(Color::Cyan) .foreground(Color::Cyan)
.rewind(true) .rewind(true)
.title("Default protocol", Alignment::Left) .title(Title::from("Default protocol").alignment(HorizontalAlignment::Left))
.value(match protocol { .value(match protocol {
FileTransferProtocol::Sftp => RADIO_PROTOCOL_SFTP, FileTransferProtocol::Sftp => RADIO_PROTOCOL_SFTP,
FileTransferProtocol::Scp => RADIO_PROTOCOL_SCP, FileTransferProtocol::Scp => RADIO_PROTOCOL_SCP,
@@ -85,8 +91,8 @@ impl DefaultProtocol {
} }
} }
impl Component<Msg, NoUserEvent> for DefaultProtocol { impl AppComponent<Msg, NoUserEvent> for DefaultProtocol {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev( handle_radio_ev(
self, self,
ev, ev,
@@ -96,7 +102,7 @@ impl Component<Msg, NoUserEvent> for DefaultProtocol {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct GroupDirs { pub struct GroupDirs {
component: Radio, component: Radio,
} }
@@ -105,15 +111,19 @@ impl GroupDirs {
pub fn new(opt: Option<GroupDirsEnum>) -> Self { pub fn new(opt: Option<GroupDirsEnum>) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(Color::LightMagenta)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(Color::LightMagenta) .color(Color::LightMagenta)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.choices(["Display first", "Display last", "No"]) .choices(["Display first", "Display last", "No"])
.foreground(Color::LightMagenta)
.rewind(true) .rewind(true)
.title("Group directories", Alignment::Left) .title(Title::from("Group directories").alignment(HorizontalAlignment::Left))
.value(match opt { .value(match opt {
Some(GroupDirsEnum::First) => 0, Some(GroupDirsEnum::First) => 0,
Some(GroupDirsEnum::Last) => 1, Some(GroupDirsEnum::Last) => 1,
@@ -123,8 +133,8 @@ impl GroupDirs {
} }
} }
impl Component<Msg, NoUserEvent> for GroupDirs { impl AppComponent<Msg, NoUserEvent> for GroupDirs {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev( handle_radio_ev(
self, self,
ev, ev,
@@ -134,7 +144,7 @@ impl Component<Msg, NoUserEvent> for GroupDirs {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct HiddenFiles { pub struct HiddenFiles {
component: Radio, component: Radio,
} }
@@ -151,14 +161,17 @@ impl HiddenFiles {
.choices(["Yes", "No"]) .choices(["Yes", "No"])
.foreground(Color::LightRed) .foreground(Color::LightRed)
.rewind(true) .rewind(true)
.title("Show hidden files? (by default)", Alignment::Left) .title(
Title::from("Show hidden files? (by default)")
.alignment(HorizontalAlignment::Left),
)
.value(usize::from(!enabled)), .value(usize::from(!enabled)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for HiddenFiles { impl AppComponent<Msg, NoUserEvent> for HiddenFiles {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev( handle_radio_ev(
self, self,
ev, ev,
@@ -168,7 +181,7 @@ impl Component<Msg, NoUserEvent> for HiddenFiles {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct NotificationsEnabled { pub struct NotificationsEnabled {
component: Radio, component: Radio,
} }
@@ -177,22 +190,26 @@ impl NotificationsEnabled {
pub fn new(enabled: bool) -> Self { pub fn new(enabled: bool) -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(Color::LightRed)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(Color::LightRed) .color(Color::LightRed)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.choices(["Yes", "No"]) .choices(["Yes", "No"])
.foreground(Color::LightRed)
.rewind(true) .rewind(true)
.title("Enable notifications?", Alignment::Left) .title(Title::from("Enable notifications?").alignment(HorizontalAlignment::Left))
.value(usize::from(!enabled)), .value(usize::from(!enabled)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for NotificationsEnabled { impl AppComponent<Msg, NoUserEvent> for NotificationsEnabled {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev( handle_radio_ev(
self, self,
ev, ev,
@@ -202,7 +219,7 @@ impl Component<Msg, NoUserEvent> for NotificationsEnabled {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct PromptOnFileReplace { pub struct PromptOnFileReplace {
component: Radio, component: Radio,
} }
@@ -219,14 +236,17 @@ impl PromptOnFileReplace {
.choices(["Yes", "No"]) .choices(["Yes", "No"])
.foreground(Color::LightBlue) .foreground(Color::LightBlue)
.rewind(true) .rewind(true)
.title("Prompt when replacing existing files?", Alignment::Left) .title(
Title::from("Prompt when replacing existing files?")
.alignment(HorizontalAlignment::Left),
)
.value(usize::from(!enabled)), .value(usize::from(!enabled)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for PromptOnFileReplace { impl AppComponent<Msg, NoUserEvent> for PromptOnFileReplace {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
handle_radio_ev( handle_radio_ev(
self, self,
ev, ev,
@@ -236,7 +256,7 @@ impl Component<Msg, NoUserEvent> for PromptOnFileReplace {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct LocalFileFmt { pub struct LocalFileFmt {
component: Input, component: Input,
} }
@@ -252,18 +272,21 @@ impl LocalFileFmt {
) )
.foreground(Color::LightGreen) .foreground(Color::LightGreen)
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}", "{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(
Title::from("File formatter syntax (local)")
.alignment(HorizontalAlignment::Left),
) )
.title("File formatter syntax (local)", Alignment::Left)
.value(value), .value(value),
} }
} }
} }
impl Component<Msg, NoUserEvent> for LocalFileFmt { impl AppComponent<Msg, NoUserEvent> for LocalFileFmt {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev( handle_input_ev(
self, self,
ev, ev,
@@ -273,7 +296,7 @@ impl Component<Msg, NoUserEvent> for LocalFileFmt {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct NotificationsThreshold { pub struct NotificationsThreshold {
component: Input, component: Input,
} }
@@ -297,15 +320,21 @@ impl NotificationsThreshold {
.foreground(Color::LightYellow) .foreground(Color::LightYellow)
.invalid_style(Style::default().fg(Color::Red)) .invalid_style(Style::default().fg(Color::Red))
.input_type(InputType::Custom(validate, char_valid)) .input_type(InputType::Custom(validate, char_valid))
.placeholder("64 MB", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title("Notifications: minimum transfer size", Alignment::Left) "64 MB",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(
Title::from("Notifications: minimum transfer size")
.alignment(HorizontalAlignment::Left),
)
.value(value), .value(value),
} }
} }
} }
impl Component<Msg, NoUserEvent> for NotificationsThreshold { impl AppComponent<Msg, NoUserEvent> for NotificationsThreshold {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev( handle_input_ev(
self, self,
ev, ev,
@@ -315,7 +344,7 @@ impl Component<Msg, NoUserEvent> for NotificationsThreshold {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct RemoteFileFmt { pub struct RemoteFileFmt {
component: Input, component: Input,
} }
@@ -331,18 +360,21 @@ impl RemoteFileFmt {
) )
.foreground(Color::Cyan) .foreground(Color::Cyan)
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}", "{NAME:36} {PEX} {SIZE} {MTIME:17:%b %d %Y %H:%M}",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(
Title::from("File formatter syntax (remote)")
.alignment(HorizontalAlignment::Left),
) )
.title("File formatter syntax (remote)", Alignment::Left)
.value(value), .value(value),
} }
} }
} }
impl Component<Msg, NoUserEvent> for RemoteFileFmt { impl AppComponent<Msg, NoUserEvent> for RemoteFileFmt {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev( handle_input_ev(
self, self,
ev, ev,
@@ -352,7 +384,7 @@ impl Component<Msg, NoUserEvent> for RemoteFileFmt {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct SshConfig { pub struct SshConfig {
component: Input, component: Input,
} }
@@ -368,18 +400,18 @@ impl SshConfig {
) )
.foreground(Color::LightBlue) .foreground(Color::LightBlue)
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"~/.ssh/config", "~/.ssh/config",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
) ))
.title("SSH configuration path", Alignment::Left) .title(Title::from("SSH configuration path").alignment(HorizontalAlignment::Left))
.value(value), .value(value),
} }
} }
} }
impl Component<Msg, NoUserEvent> for SshConfig { impl AppComponent<Msg, NoUserEvent> for SshConfig {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev( handle_input_ev(
self, self,
ev, ev,
@@ -389,7 +421,7 @@ impl Component<Msg, NoUserEvent> for SshConfig {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct TextEditor { pub struct TextEditor {
component: Input, component: Input,
} }
@@ -405,15 +437,18 @@ impl TextEditor {
) )
.foreground(Color::LightGreen) .foreground(Color::LightGreen)
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder("vim", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title("Text editor", Alignment::Left) "vim",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from("Text editor").alignment(HorizontalAlignment::Left))
.value(value), .value(value),
} }
} }
} }
impl Component<Msg, NoUserEvent> for TextEditor { impl AppComponent<Msg, NoUserEvent> for TextEditor {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
handle_input_ev( handle_input_ev(
self, self,
ev, ev,
@@ -426,8 +461,8 @@ impl Component<Msg, NoUserEvent> for TextEditor {
// -- event handler // -- event handler
fn handle_input_ev( fn handle_input_ev(
component: &mut dyn Component<Msg, NoUserEvent>, component: &mut dyn AppComponent<Msg, NoUserEvent>,
ev: Event<NoUserEvent>, ev: &Event<NoUserEvent>,
on_key_down: Msg, on_key_down: Msg,
on_key_up: Msg, on_key_up: Msg,
) -> Option<Msg> { ) -> Option<Msg> {
@@ -476,7 +511,7 @@ fn handle_input_ev(
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
component.perform(Cmd::Type(ch)); component.perform(Cmd::Type(*ch));
Some(Msg::Config(ConfigMsg::ConfigChanged)) Some(Msg::Config(ConfigMsg::ConfigChanged))
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
@@ -488,8 +523,8 @@ fn handle_input_ev(
} }
fn handle_radio_ev( fn handle_radio_ev(
component: &mut dyn Component<Msg, NoUserEvent>, component: &mut dyn AppComponent<Msg, NoUserEvent>,
ev: Event<NoUserEvent>, ev: &Event<NoUserEvent>,
on_key_down: Msg, on_key_down: Msg,
on_key_up: Msg, on_key_up: Msg,
) -> Option<Msg> { ) -> Option<Msg> {
+45 -30
View File
@@ -2,13 +2,15 @@
//! //!
//! ssh components //! ssh components
use tui_realm_stdlib::{Input, List, Radio}; use tui_realm_stdlib::components::{Input, List, Radio};
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::props::{ use tuirealm::props::{
Alignment, BorderSides, BorderType, Borders, Color, InputType, Style, TextSpan, BorderSides, BorderType, Borders, Color, HorizontalAlignment, InputType, SpanStatic, Style,
TextModifiers, Title,
}; };
use tuirealm::{Component, Event, MockComponent, NoUserEvent, State, StateValue}; use tuirealm::state::{State, StateValue};
use super::{Msg, SshMsg}; use super::{Msg, SshMsg};
@@ -17,7 +19,7 @@ SshHost,
SshKeys, SshKeys,
SshUsername, */ SshUsername, */
#[derive(MockComponent)] #[derive(Component)]
pub struct DelSshKeyPopup { pub struct DelSshKeyPopup {
component: Radio, component: Radio,
} }
@@ -26,22 +28,26 @@ impl Default for DelSshKeyPopup {
fn default() -> Self { fn default() -> Self {
Self { Self {
component: Radio::default() component: Radio::default()
.highlight_style(
Style::default()
.fg(Color::Red)
.add_modifier(TextModifiers::REVERSED),
)
.borders( .borders(
Borders::default() Borders::default()
.color(Color::Red) .color(Color::Red)
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.choices(["Yes", "No"]) .choices(["Yes", "No"])
.foreground(Color::Red)
.rewind(true) .rewind(true)
.title("Delete key?", Alignment::Center) .title(Title::from("Delete key?").alignment(HorizontalAlignment::Center))
.value(1), .value(1),
} }
} }
} }
impl Component<Msg, NoUserEvent> for DelSshKeyPopup { impl AppComponent<Msg, NoUserEvent> for DelSshKeyPopup {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => { Event::Keyboard(KeyEvent { code: Key::Esc, .. }) => {
Some(Msg::Ssh(SshMsg::CloseDelSshKeyPopup)) Some(Msg::Ssh(SshMsg::CloseDelSshKeyPopup))
@@ -71,7 +77,7 @@ impl Component<Msg, NoUserEvent> for DelSshKeyPopup {
}) => { }) => {
if matches!( if matches!(
self.perform(Cmd::Submit), self.perform(Cmd::Submit),
CmdResult::Submit(State::One(StateValue::Usize(0))) CmdResult::Submit(State::Single(StateValue::Usize(0)))
) { ) {
Some(Msg::Ssh(SshMsg::DeleteSshKey)) Some(Msg::Ssh(SshMsg::DeleteSshKey))
} else { } else {
@@ -83,7 +89,7 @@ impl Component<Msg, NoUserEvent> for DelSshKeyPopup {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct SshKeys { pub struct SshKeys {
component: List, component: List,
} }
@@ -98,18 +104,22 @@ impl SshKeys {
.modifiers(BorderType::Rounded), .modifiers(BorderType::Rounded),
) )
.foreground(Color::LightGreen) .foreground(Color::LightGreen)
.highlighted_color(Color::LightGreen) .highlight_style(Style::default().fg(Color::LightGreen))
.rewind(true) .rewind(true)
.rows(keys.iter().map(|x| vec![TextSpan::from(x)]).collect()) .rows(
keys.iter()
.map(|x| vec![SpanStatic::from(x.clone())])
.collect::<Vec<Vec<SpanStatic>>>(),
)
.step(4) .step(4)
.scroll(true) .scroll(true)
.title("SSH Keys", Alignment::Left), .title(Title::from("SSH Keys").alignment(HorizontalAlignment::Left)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for SshKeys { impl AppComponent<Msg, NoUserEvent> for SshKeys {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Down, .. code: Key::Down, ..
@@ -147,7 +157,9 @@ impl Component<Msg, NoUserEvent> for SshKeys {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Enter, .. code: Key::Enter, ..
}) => match self.state() { }) => match self.state() {
State::One(StateValue::Usize(choice)) => Some(Msg::Ssh(SshMsg::EditSshKey(choice))), State::Single(StateValue::Usize(choice)) => {
Some(Msg::Ssh(SshMsg::EditSshKey(choice)))
}
_ => Some(Msg::None), _ => Some(Msg::None),
}, },
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
@@ -163,7 +175,7 @@ impl Component<Msg, NoUserEvent> for SshKeys {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct SshHost { pub struct SshHost {
component: Input, component: Input,
} }
@@ -177,17 +189,17 @@ impl Default for SshHost {
.sides(BorderSides::TOP | BorderSides::RIGHT | BorderSides::LEFT), .sides(BorderSides::TOP | BorderSides::RIGHT | BorderSides::LEFT),
) )
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder( .placeholder(tuirealm::props::SpanStatic::styled(
"192.168.1.2", "192.168.1.2",
Style::default().fg(Color::Rgb(128, 128, 128)), Style::default().fg(Color::Rgb(128, 128, 128)),
) ))
.title("Hostname or address", Alignment::Center), .title(Title::from("Hostname or address").alignment(HorizontalAlignment::Center)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for SshHost { impl AppComponent<Msg, NoUserEvent> for SshHost {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -233,7 +245,7 @@ impl Component<Msg, NoUserEvent> for SshHost {
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
self.perform(Cmd::Type(ch)); self.perform(Cmd::Type(*ch));
Some(Msg::None) Some(Msg::None)
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
@@ -250,7 +262,7 @@ impl Component<Msg, NoUserEvent> for SshHost {
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct SshUsername { pub struct SshUsername {
component: Input, component: Input,
} }
@@ -264,14 +276,17 @@ impl Default for SshUsername {
.sides(BorderSides::BOTTOM | BorderSides::RIGHT | BorderSides::LEFT), .sides(BorderSides::BOTTOM | BorderSides::RIGHT | BorderSides::LEFT),
) )
.input_type(InputType::Text) .input_type(InputType::Text)
.placeholder("root", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title("Username", Alignment::Center), "root",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from("Username").alignment(HorizontalAlignment::Center)),
} }
} }
} }
impl Component<Msg, NoUserEvent> for SshUsername { impl AppComponent<Msg, NoUserEvent> for SshUsername {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -312,7 +327,7 @@ impl Component<Msg, NoUserEvent> for SshUsername {
code: Key::Char(ch), code: Key::Char(ch),
modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT, modifiers: KeyModifiers::NONE | KeyModifiers::SHIFT,
}) => { }) => {
self.perform(Cmd::Type(ch)); self.perform(Cmd::Type(*ch));
Some(Msg::None) Some(Msg::None)
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
+107 -102
View File
@@ -2,20 +2,22 @@
//! //!
//! theme tab components //! theme tab components
use tui_realm_stdlib::{Input, Label}; use tui_realm_stdlib::components::{Input, Label};
use tuirealm::command::{Cmd, CmdResult, Direction, Position}; use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::component::{AppComponent, Component};
use tuirealm::props::{Alignment, BorderType, Borders, Color, InputType, Style, TextModifiers}; use tuirealm::event::{Event, Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::{ use tuirealm::props::{
AttrValue, Attribute, Component, Event, MockComponent, NoUserEvent, State, StateValue, AttrValue, Attribute, BorderType, Borders, Color, HorizontalAlignment, InputType, Style,
TextModifiers, Title,
}; };
use tuirealm::state::{State, StateValue};
use super::{Msg, ThemeMsg}; use super::{Msg, ThemeMsg};
use crate::ui::activities::setup::IdTheme; use crate::ui::activities::setup::IdTheme;
// -- components // -- components
#[derive(MockComponent)] #[derive(Component)]
pub struct AuthTitle { pub struct AuthTitle {
component: Label, component: Label,
} }
@@ -30,13 +32,13 @@ impl Default for AuthTitle {
} }
} }
impl Component<Msg, NoUserEvent> for AuthTitle { impl AppComponent<Msg, NoUserEvent> for AuthTitle {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct MiscTitle { pub struct MiscTitle {
component: Label, component: Label,
} }
@@ -51,13 +53,13 @@ impl Default for MiscTitle {
} }
} }
impl Component<Msg, NoUserEvent> for MiscTitle { impl AppComponent<Msg, NoUserEvent> for MiscTitle {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct TransferTitle { pub struct TransferTitle {
component: Label, component: Label,
} }
@@ -72,13 +74,13 @@ impl Default for TransferTitle {
} }
} }
impl Component<Msg, NoUserEvent> for TransferTitle { impl AppComponent<Msg, NoUserEvent> for TransferTitle {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct TransferTitle2 { pub struct TransferTitle2 {
component: Label, component: Label,
} }
@@ -93,13 +95,13 @@ impl Default for TransferTitle2 {
} }
} }
impl Component<Msg, NoUserEvent> for TransferTitle2 { impl AppComponent<Msg, NoUserEvent> for TransferTitle2 {
fn on(&mut self, _ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, _ev: &Event<NoUserEvent>) -> Option<Msg> {
None None
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct AuthAddress { pub struct AuthAddress {
component: InputColor, component: InputColor,
} }
@@ -118,13 +120,13 @@ impl AuthAddress {
} }
} }
impl Component<Msg, NoUserEvent> for AuthAddress { impl AppComponent<Msg, NoUserEvent> for AuthAddress {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct AuthBookmarks { pub struct AuthBookmarks {
component: InputColor, component: InputColor,
} }
@@ -143,13 +145,13 @@ impl AuthBookmarks {
} }
} }
impl Component<Msg, NoUserEvent> for AuthBookmarks { impl AppComponent<Msg, NoUserEvent> for AuthBookmarks {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct AuthPassword { pub struct AuthPassword {
component: InputColor, component: InputColor,
} }
@@ -168,13 +170,13 @@ impl AuthPassword {
} }
} }
impl Component<Msg, NoUserEvent> for AuthPassword { impl AppComponent<Msg, NoUserEvent> for AuthPassword {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct AuthPort { pub struct AuthPort {
component: InputColor, component: InputColor,
} }
@@ -193,13 +195,13 @@ impl AuthPort {
} }
} }
impl Component<Msg, NoUserEvent> for AuthPort { impl AppComponent<Msg, NoUserEvent> for AuthPort {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct AuthProtocol { pub struct AuthProtocol {
component: InputColor, component: InputColor,
} }
@@ -218,13 +220,13 @@ impl AuthProtocol {
} }
} }
impl Component<Msg, NoUserEvent> for AuthProtocol { impl AppComponent<Msg, NoUserEvent> for AuthProtocol {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct AuthRecentHosts { pub struct AuthRecentHosts {
component: InputColor, component: InputColor,
} }
@@ -243,12 +245,12 @@ impl AuthRecentHosts {
} }
} }
impl Component<Msg, NoUserEvent> for AuthRecentHosts { impl AppComponent<Msg, NoUserEvent> for AuthRecentHosts {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct AuthUsername { pub struct AuthUsername {
component: InputColor, component: InputColor,
} }
@@ -267,13 +269,13 @@ impl AuthUsername {
} }
} }
impl Component<Msg, NoUserEvent> for AuthUsername { impl AppComponent<Msg, NoUserEvent> for AuthUsername {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct ExplorerLocalBg { pub struct ExplorerLocalBg {
component: InputColor, component: InputColor,
} }
@@ -292,13 +294,13 @@ impl ExplorerLocalBg {
} }
} }
impl Component<Msg, NoUserEvent> for ExplorerLocalBg { impl AppComponent<Msg, NoUserEvent> for ExplorerLocalBg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct ExplorerLocalFg { pub struct ExplorerLocalFg {
component: InputColor, component: InputColor,
} }
@@ -317,13 +319,13 @@ impl ExplorerLocalFg {
} }
} }
impl Component<Msg, NoUserEvent> for ExplorerLocalFg { impl AppComponent<Msg, NoUserEvent> for ExplorerLocalFg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct ExplorerLocalHg { pub struct ExplorerLocalHg {
component: InputColor, component: InputColor,
} }
@@ -342,13 +344,13 @@ impl ExplorerLocalHg {
} }
} }
impl Component<Msg, NoUserEvent> for ExplorerLocalHg { impl AppComponent<Msg, NoUserEvent> for ExplorerLocalHg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct ExplorerRemoteBg { pub struct ExplorerRemoteBg {
component: InputColor, component: InputColor,
} }
@@ -367,13 +369,13 @@ impl ExplorerRemoteBg {
} }
} }
impl Component<Msg, NoUserEvent> for ExplorerRemoteBg { impl AppComponent<Msg, NoUserEvent> for ExplorerRemoteBg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct ExplorerRemoteFg { pub struct ExplorerRemoteFg {
component: InputColor, component: InputColor,
} }
@@ -392,13 +394,13 @@ impl ExplorerRemoteFg {
} }
} }
impl Component<Msg, NoUserEvent> for ExplorerRemoteFg { impl AppComponent<Msg, NoUserEvent> for ExplorerRemoteFg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct ExplorerRemoteHg { pub struct ExplorerRemoteHg {
component: InputColor, component: InputColor,
} }
@@ -417,13 +419,13 @@ impl ExplorerRemoteHg {
} }
} }
impl Component<Msg, NoUserEvent> for ExplorerRemoteHg { impl AppComponent<Msg, NoUserEvent> for ExplorerRemoteHg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct LogBg { pub struct LogBg {
component: InputColor, component: InputColor,
} }
@@ -442,13 +444,13 @@ impl LogBg {
} }
} }
impl Component<Msg, NoUserEvent> for LogBg { impl AppComponent<Msg, NoUserEvent> for LogBg {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct LogWindow { pub struct LogWindow {
component: InputColor, component: InputColor,
} }
@@ -467,13 +469,13 @@ impl LogWindow {
} }
} }
impl Component<Msg, NoUserEvent> for LogWindow { impl AppComponent<Msg, NoUserEvent> for LogWindow {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct MiscError { pub struct MiscError {
component: InputColor, component: InputColor,
} }
@@ -492,13 +494,13 @@ impl MiscError {
} }
} }
impl Component<Msg, NoUserEvent> for MiscError { impl AppComponent<Msg, NoUserEvent> for MiscError {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct MiscInfo { pub struct MiscInfo {
component: InputColor, component: InputColor,
} }
@@ -517,13 +519,13 @@ impl MiscInfo {
} }
} }
impl Component<Msg, NoUserEvent> for MiscInfo { impl AppComponent<Msg, NoUserEvent> for MiscInfo {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct MiscInput { pub struct MiscInput {
component: InputColor, component: InputColor,
} }
@@ -542,13 +544,13 @@ impl MiscInput {
} }
} }
impl Component<Msg, NoUserEvent> for MiscInput { impl AppComponent<Msg, NoUserEvent> for MiscInput {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct MiscKeys { pub struct MiscKeys {
component: InputColor, component: InputColor,
} }
@@ -567,13 +569,13 @@ impl MiscKeys {
} }
} }
impl Component<Msg, NoUserEvent> for MiscKeys { impl AppComponent<Msg, NoUserEvent> for MiscKeys {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct MiscQuit { pub struct MiscQuit {
component: InputColor, component: InputColor,
} }
@@ -592,13 +594,13 @@ impl MiscQuit {
} }
} }
impl Component<Msg, NoUserEvent> for MiscQuit { impl AppComponent<Msg, NoUserEvent> for MiscQuit {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct MiscSave { pub struct MiscSave {
component: InputColor, component: InputColor,
} }
@@ -617,13 +619,13 @@ impl MiscSave {
} }
} }
impl Component<Msg, NoUserEvent> for MiscSave { impl AppComponent<Msg, NoUserEvent> for MiscSave {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct MiscWarn { pub struct MiscWarn {
component: InputColor, component: InputColor,
} }
@@ -642,13 +644,13 @@ impl MiscWarn {
} }
} }
impl Component<Msg, NoUserEvent> for MiscWarn { impl AppComponent<Msg, NoUserEvent> for MiscWarn {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct ProgBar { pub struct ProgBar {
component: InputColor, component: InputColor,
} }
@@ -667,13 +669,13 @@ impl ProgBar {
} }
} }
impl Component<Msg, NoUserEvent> for ProgBar { impl AppComponent<Msg, NoUserEvent> for ProgBar {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct StatusHidden { pub struct StatusHidden {
component: InputColor, component: InputColor,
} }
@@ -692,13 +694,13 @@ impl StatusHidden {
} }
} }
impl Component<Msg, NoUserEvent> for StatusHidden { impl AppComponent<Msg, NoUserEvent> for StatusHidden {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct StatusSorting { pub struct StatusSorting {
component: InputColor, component: InputColor,
} }
@@ -717,13 +719,13 @@ impl StatusSorting {
} }
} }
impl Component<Msg, NoUserEvent> for StatusSorting { impl AppComponent<Msg, NoUserEvent> for StatusSorting {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
#[derive(MockComponent)] #[derive(Component)]
pub struct StatusSync { pub struct StatusSync {
component: InputColor, component: InputColor,
} }
@@ -742,15 +744,15 @@ impl StatusSync {
} }
} }
impl Component<Msg, NoUserEvent> for StatusSync { impl AppComponent<Msg, NoUserEvent> for StatusSync {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
self.component.on(ev) self.component.on(ev)
} }
} }
// -- input color // -- input color
#[derive(MockComponent)] #[derive(Component)]
struct InputColor { struct InputColor {
component: Input, component: Input,
id: IdTheme, id: IdTheme,
@@ -770,8 +772,11 @@ impl InputColor {
) )
.foreground(color) .foreground(color)
.input_type(InputType::Color) .input_type(InputType::Color)
.placeholder("#aa33ee", Style::default().fg(Color::Rgb(128, 128, 128))) .placeholder(tuirealm::props::SpanStatic::styled(
.title(name, Alignment::Left) "#aa33ee",
Style::default().fg(Color::Rgb(128, 128, 128)),
))
.title(Title::from(name.to_string()).alignment(HorizontalAlignment::Left))
.value(value), .value(value),
id, id,
on_key_down, on_key_down,
@@ -780,7 +785,7 @@ impl InputColor {
} }
fn update_color(&mut self, result: CmdResult) -> Option<Msg> { fn update_color(&mut self, result: CmdResult) -> Option<Msg> {
if let CmdResult::Changed(State::One(StateValue::String(color))) = result { if let CmdResult::Changed(State::Single(StateValue::String(color))) = result {
let color = tuirealm::utils::parser::parse_color(&color).unwrap(); let color = tuirealm::utils::parser::parse_color(&color).unwrap();
self.attr(Attribute::Foreground, AttrValue::Color(color)); self.attr(Attribute::Foreground, AttrValue::Color(color));
self.attr( self.attr(
@@ -807,8 +812,8 @@ impl InputColor {
} }
} }
impl Component<Msg, NoUserEvent> for InputColor { impl AppComponent<Msg, NoUserEvent> for InputColor {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> { fn on(&mut self, ev: &Event<NoUserEvent>) -> Option<Msg> {
match ev { match ev {
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
code: Key::Left, .. code: Key::Left, ..
@@ -854,7 +859,7 @@ impl Component<Msg, NoUserEvent> for InputColor {
code: Key::Char(ch), code: Key::Char(ch),
.. ..
}) => { }) => {
let result = self.perform(Cmd::Type(ch)); let result = self.perform(Cmd::Type(*ch));
self.update_color(result) self.update_color(result)
} }
Event::Keyboard(KeyEvent { Event::Keyboard(KeyEvent {
+2
View File
@@ -7,6 +7,8 @@
// Ext // Ext
use std::env; use std::env;
use tuirealm::terminal::TerminalAdapter;
use super::SetupActivity; use super::SetupActivity;
impl SetupActivity { impl SetupActivity {
+2 -3
View File
@@ -5,17 +5,16 @@
// locals // locals
// ext // ext
use tuirealm::Update;
use super::{ use super::{
CommonMsg, ConfigMsg, Id, IdConfig, IdSsh, IdTheme, Msg, SetupActivity, SshMsg, ThemeMsg, CommonMsg, ConfigMsg, Id, IdConfig, IdSsh, IdTheme, Msg, SetupActivity, SshMsg, ThemeMsg,
ViewLayout, ViewLayout,
}; };
impl Update<Msg> for SetupActivity { impl SetupActivity {
/// Update auth activity model based on msg /// Update auth activity model based on msg
/// The function exits when returns None /// The function exits when returns None
fn update(&mut self, msg: Option<Msg>) -> Option<Msg> { pub(super) fn update(&mut self, msg: Option<Msg>) -> Option<Msg> {
match msg.unwrap_or(Msg::None) { match msg.unwrap_or(Msg::None) {
Msg::Common(msg) => self.common_update(msg), Msg::Common(msg) => self.common_update(msg),
Msg::Config(msg) => self.config_update(msg), Msg::Config(msg) => self.config_update(msg),
+11 -10
View File
@@ -8,8 +8,9 @@ pub mod ssh_keys;
pub mod theme; pub mod theme;
use tuirealm::event::{Key, KeyEvent, KeyModifiers}; use tuirealm::event::{Key, KeyEvent, KeyModifiers};
use tuirealm::ratatui::Frame;
use tuirealm::ratatui::widgets::Clear; use tuirealm::ratatui::widgets::Clear;
use tuirealm::{Frame, Sub, SubClause, SubEventClause}; use tuirealm::subscription::{EventClause, Sub, SubClause};
use super::*; use super::*;
use crate::utils::ui::{Popup, Size}; use crate::utils::ui::{Popup, Size};
@@ -172,62 +173,62 @@ impl SetupActivity {
Box::<components::GlobalListener>::default(), Box::<components::GlobalListener>::default(),
vec![ vec![
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Esc, code: Key::Esc,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Function(10), code: Key::Function(10),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Tab, code: Key::Tab,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Char('h'), code: Key::Char('h'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Function(1), code: Key::Function(1),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Char('r'), code: Key::Char('r'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Char('s'), code: Key::Char('s'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new( Sub::new(
SubEventClause::Keyboard(KeyEvent { EventClause::Keyboard(KeyEvent {
code: Key::Function(4), code: Key::Function(4),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
}), }),
Self::no_popup_mounted_clause(), Self::no_popup_mounted_clause(),
), ),
Sub::new(SubEventClause::WindowResize, SubClause::Always), Sub::new(EventClause::WindowResize, SubClause::Always),
], ],
) { ) {
error!("Failed to mount component: {err}"); error!("Failed to mount component: {err}");
+13 -12
View File
@@ -8,7 +8,8 @@
use std::path::PathBuf; use std::path::PathBuf;
use tuirealm::ratatui::layout::{Constraint, Direction, Layout}; use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
use tuirealm::{State, StateValue}; use tuirealm::state::{State, StateValue};
use tuirealm::terminal::TerminalAdapter;
use super::{ use super::{
Context, Id, IdCommon, IdConfig, RADIO_PROTOCOL_KUBE, RADIO_PROTOCOL_WEBDAV, SetupActivity, Context, Id, IdCommon, IdConfig, RADIO_PROTOCOL_KUBE, RADIO_PROTOCOL_WEBDAV, SetupActivity,
@@ -256,13 +257,13 @@ impl SetupActivity {
/// Collect values from input and put them into the configuration /// Collect values from input and put them into the configuration
pub(crate) fn collect_input_values(&mut self) { pub(crate) fn collect_input_values(&mut self) {
if let Ok(State::One(StateValue::String(editor))) = if let Ok(State::Single(StateValue::String(editor))) =
self.app.state(&Id::Config(IdConfig::TextEditor)) self.app.state(&Id::Config(IdConfig::TextEditor))
{ {
self.config_mut() self.config_mut()
.set_text_editor(PathBuf::from(editor.as_str())); .set_text_editor(PathBuf::from(editor.as_str()));
} }
if let Ok(State::One(StateValue::Usize(protocol))) = if let Ok(State::Single(StateValue::Usize(protocol))) =
self.app.state(&Id::Config(IdConfig::DefaultProtocol)) self.app.state(&Id::Config(IdConfig::DefaultProtocol))
{ {
let protocol: FileTransferProtocol = match protocol { let protocol: FileTransferProtocol = match protocol {
@@ -277,35 +278,35 @@ impl SetupActivity {
}; };
self.config_mut().set_default_protocol(protocol); self.config_mut().set_default_protocol(protocol);
} }
if let Ok(State::One(StateValue::Usize(opt))) = if let Ok(State::Single(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::HiddenFiles)) self.app.state(&Id::Config(IdConfig::HiddenFiles))
{ {
let show: bool = matches!(opt, 0); let show: bool = matches!(opt, 0);
self.config_mut().set_show_hidden_files(show); self.config_mut().set_show_hidden_files(show);
} }
if let Ok(State::One(StateValue::Usize(opt))) = if let Ok(State::Single(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::CheckUpdates)) self.app.state(&Id::Config(IdConfig::CheckUpdates))
{ {
let check: bool = matches!(opt, 0); let check: bool = matches!(opt, 0);
self.config_mut().set_check_for_updates(check); self.config_mut().set_check_for_updates(check);
} }
if let Ok(State::One(StateValue::Usize(opt))) = if let Ok(State::Single(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::PromptOnFileReplace)) self.app.state(&Id::Config(IdConfig::PromptOnFileReplace))
{ {
let check: bool = matches!(opt, 0); let check: bool = matches!(opt, 0);
self.config_mut().set_prompt_on_file_replace(check); self.config_mut().set_prompt_on_file_replace(check);
} }
if let Ok(State::One(StateValue::String(fmt))) = if let Ok(State::Single(StateValue::String(fmt))) =
self.app.state(&Id::Config(IdConfig::LocalFileFmt)) self.app.state(&Id::Config(IdConfig::LocalFileFmt))
{ {
self.config_mut().set_local_file_fmt(fmt); self.config_mut().set_local_file_fmt(fmt);
} }
if let Ok(State::One(StateValue::String(fmt))) = if let Ok(State::Single(StateValue::String(fmt))) =
self.app.state(&Id::Config(IdConfig::RemoteFileFmt)) self.app.state(&Id::Config(IdConfig::RemoteFileFmt))
{ {
self.config_mut().set_remote_file_fmt(fmt); self.config_mut().set_remote_file_fmt(fmt);
} }
if let Ok(State::One(StateValue::Usize(opt))) = if let Ok(State::Single(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::GroupDirs)) self.app.state(&Id::Config(IdConfig::GroupDirs))
{ {
let dirs: Option<GroupDirs> = match opt { let dirs: Option<GroupDirs> = match opt {
@@ -315,18 +316,18 @@ impl SetupActivity {
}; };
self.config_mut().set_group_dirs(dirs); self.config_mut().set_group_dirs(dirs);
} }
if let Ok(State::One(StateValue::Usize(opt))) = if let Ok(State::Single(StateValue::Usize(opt))) =
self.app.state(&Id::Config(IdConfig::NotificationsEnabled)) self.app.state(&Id::Config(IdConfig::NotificationsEnabled))
{ {
self.config_mut().set_notifications(opt == 0); self.config_mut().set_notifications(opt == 0);
} }
if let Ok(State::One(StateValue::U64(bytes))) = self if let Ok(State::Single(StateValue::U64(bytes))) = self
.app .app
.state(&Id::Config(IdConfig::NotificationsThreshold)) .state(&Id::Config(IdConfig::NotificationsThreshold))
{ {
self.config_mut().set_notification_threshold(bytes); self.config_mut().set_notification_threshold(bytes);
} }
if let Ok(State::One(StateValue::String(mut path))) = if let Ok(State::Single(StateValue::String(mut path))) =
self.app.state(&Id::Config(IdConfig::SshConfig)) self.app.state(&Id::Config(IdConfig::SshConfig))
{ {
if path.is_empty() { if path.is_empty() {
+1
View File
@@ -7,6 +7,7 @@
// Ext // Ext
use tuirealm::ratatui::layout::{Constraint, Direction, Layout}; use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
use tuirealm::ratatui::widgets::Clear; use tuirealm::ratatui::widgets::Clear;
use tuirealm::terminal::TerminalAdapter;
use super::{Context, Id, IdCommon, IdSsh, SetupActivity, ViewLayout, components}; use super::{Context, Id, IdCommon, IdSsh, SetupActivity, ViewLayout, components};
use crate::utils::ui::{Popup, Size}; use crate::utils::ui::{Popup, Size};
+1
View File
@@ -6,6 +6,7 @@
// Locals // Locals
// Ext // Ext
use tuirealm::ratatui::layout::{Constraint, Direction, Layout}; use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
use tuirealm::terminal::TerminalAdapter;
use super::{Context, Id, IdCommon, IdTheme, SetupActivity, Theme, ViewLayout, components}; use super::{Context, Id, IdCommon, IdTheme, SetupActivity, Theme, ViewLayout, components};
+10 -4
View File
@@ -3,7 +3,7 @@
//! `Context` is the module which provides all the functionalities related to the UI data holder, called Context //! `Context` is the module which provides all the functionalities related to the UI data holder, called Context
// Locals // Locals
use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalBridge}; use tuirealm::terminal::{CrosstermTerminalAdapter, TerminalAdapter};
use super::store::Store; use super::store::Store;
use crate::filetransfer::{FileTransferParams, HostBridgeParams}; use crate::filetransfer::{FileTransferParams, HostBridgeParams};
@@ -18,7 +18,7 @@ pub struct Context {
bookmarks_client: Option<BookmarksClient>, bookmarks_client: Option<BookmarksClient>,
config_client: ConfigClient, config_client: ConfigClient,
pub(crate) store: Store, pub(crate) store: Store,
pub(crate) terminal: TerminalBridge<CrosstermTerminalAdapter>, pub(crate) terminal: CrosstermTerminalAdapter,
theme_provider: ThemeProvider, theme_provider: ThemeProvider,
error: Option<String>, error: Option<String>,
} }
@@ -31,7 +31,13 @@ impl Context {
theme_provider: ThemeProvider, theme_provider: ThemeProvider,
error: Option<String>, error: Option<String>,
) -> Context { ) -> Context {
let mut terminal = TerminalBridge::init_crossterm().expect("Could not initialize terminal"); let mut terminal = CrosstermTerminalAdapter::new().expect("Could not initialize terminal");
terminal
.enable_raw_mode()
.expect("Could not enable terminal raw mode");
terminal
.enter_alternate_screen()
.expect("Could not enter alternate screen");
let _ = terminal.disable_mouse_capture(); let _ = terminal.disable_mouse_capture();
Context { Context {
@@ -88,7 +94,7 @@ impl Context {
&mut self.theme_provider &mut self.theme_provider
} }
pub fn terminal(&mut self) -> &mut TerminalBridge<CrosstermTerminalAdapter> { pub fn terminal(&mut self) -> &mut CrosstermTerminalAdapter {
&mut self.terminal &mut self.terminal
} }
+6 -6
View File
@@ -2,26 +2,26 @@
//! //!
//! `Utils` implements utilities functions to work with layouts //! `Utils` implements utilities functions to work with layouts
use tuirealm::terminal::{TerminalAdapter, TerminalBridge}; use tuirealm::terminal::TerminalAdapter;
/// Read a secret from tty with customisable prompt /// Read a secret from tty with customisable prompt
pub fn read_secret_from_tty<T>( pub fn read_secret_from_tty<T>(
terminal_bridge: &mut TerminalBridge<T>, terminal: &mut T,
prompt: impl ToString, prompt: impl ToString,
) -> std::io::Result<Option<String>> ) -> std::io::Result<Option<String>>
where where
T: TerminalAdapter, T: TerminalAdapter,
{ {
let _ = terminal_bridge.disable_raw_mode(); let _ = terminal.disable_raw_mode();
let _ = terminal_bridge.leave_alternate_screen(); let _ = terminal.leave_alternate_screen();
let res = match rpassword::prompt_password(prompt) { let res = match rpassword::prompt_password(prompt) {
Ok(p) if p.is_empty() => Ok(None), Ok(p) if p.is_empty() => Ok(None),
Ok(p) => Ok(Some(p)), Ok(p) => Ok(Some(p)),
Err(err) => Err(err), Err(err) => Err(err),
}; };
let _ = terminal_bridge.enter_alternate_screen(); let _ = terminal.enter_alternate_screen();
let _ = terminal_bridge.enable_raw_mode(); let _ = terminal.enable_raw_mode();
res res
} }