mirror of
https://github.com/veeso/termscp.git
synced 2026-09-27 14:31:18 -07:00
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.
475 lines
17 KiB
Rust
475 lines
17 KiB
Rust
//! ## SetupActivity
|
|
//!
|
|
//! `setup_activity` is the module which implements the Setup activity, which is the activity to
|
|
//! work on termscp configuration
|
|
|
|
// Locals
|
|
// Ext
|
|
use tuirealm::ratatui::layout::{Constraint, Direction, Layout};
|
|
use tuirealm::terminal::TerminalAdapter;
|
|
|
|
use super::{Context, Id, IdCommon, IdTheme, SetupActivity, Theme, ViewLayout, components};
|
|
|
|
impl SetupActivity {
|
|
// -- view
|
|
|
|
/// Initialize theme view
|
|
pub(super) fn init_theme(&mut self) {
|
|
// Init view (and mount commons)
|
|
self.new_app(ViewLayout::Theme);
|
|
// Mount titles
|
|
self.load_titles();
|
|
// Load styles
|
|
self.load_styles();
|
|
// Active first field
|
|
if let Err(err) = self.app.active(&Id::Theme(IdTheme::AuthProtocol)) {
|
|
error!("Failed to activate component: {err}");
|
|
}
|
|
}
|
|
|
|
pub(super) fn view_theme(&mut self) {
|
|
let mut ctx: Context = self.context.take().unwrap();
|
|
let _ = ctx.terminal().raw_mut().draw(|f| {
|
|
// Prepare main chunks
|
|
let chunks = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.margin(1)
|
|
.constraints(
|
|
[
|
|
Constraint::Length(3), // Current tab
|
|
Constraint::Min(22), // Main body
|
|
Constraint::Length(1), // Help footer
|
|
]
|
|
.as_ref(),
|
|
)
|
|
.split(f.area());
|
|
// Render common widget
|
|
self.app.view(&Id::Common(IdCommon::Header), f, chunks[0]);
|
|
self.app.view(&Id::Common(IdCommon::Footer), f, chunks[2]);
|
|
// Make chunks
|
|
let colors_layout = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.constraints(
|
|
[
|
|
Constraint::Percentage(25),
|
|
Constraint::Percentage(25),
|
|
Constraint::Percentage(25),
|
|
Constraint::Percentage(25),
|
|
]
|
|
.as_ref(),
|
|
)
|
|
.split(chunks[1]);
|
|
let auth_colors_layout = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints(
|
|
[
|
|
Constraint::Length(1), // Title
|
|
Constraint::Length(3), // Protocol
|
|
Constraint::Length(3), // Addr
|
|
Constraint::Length(3), // Port
|
|
Constraint::Length(3), // Username
|
|
Constraint::Length(3), // Password
|
|
Constraint::Length(3), // Bookmarks
|
|
Constraint::Length(3), // Recents
|
|
Constraint::Length(1), // Prevent overflow
|
|
]
|
|
.as_ref(),
|
|
)
|
|
.split(colors_layout[0]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::AuthTitle), f, auth_colors_layout[0]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::AuthProtocol), f, auth_colors_layout[1]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::AuthAddress), f, auth_colors_layout[2]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::AuthPort), f, auth_colors_layout[3]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::AuthUsername), f, auth_colors_layout[4]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::AuthPassword), f, auth_colors_layout[5]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::AuthBookmarks), f, auth_colors_layout[6]);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::AuthRecentHosts),
|
|
f,
|
|
auth_colors_layout[7],
|
|
);
|
|
let misc_colors_layout = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints(
|
|
[
|
|
Constraint::Length(1), // Title
|
|
Constraint::Length(3), // Error
|
|
Constraint::Length(3), // Info
|
|
Constraint::Length(3), // Input
|
|
Constraint::Length(3), // Keys
|
|
Constraint::Length(3), // Quit
|
|
Constraint::Length(3), // Save
|
|
Constraint::Length(3), // Warn
|
|
Constraint::Length(1), // Prevent overflow
|
|
]
|
|
.as_ref(),
|
|
)
|
|
.split(colors_layout[1]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::MiscTitle), f, misc_colors_layout[0]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::MiscError), f, misc_colors_layout[1]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::MiscInfo), f, misc_colors_layout[2]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::MiscInput), f, misc_colors_layout[3]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::MiscKeys), f, misc_colors_layout[4]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::MiscQuit), f, misc_colors_layout[5]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::MiscSave), f, misc_colors_layout[6]);
|
|
self.app
|
|
.view(&Id::Theme(IdTheme::MiscWarn), f, misc_colors_layout[7]);
|
|
|
|
let transfer_colors_layout_col1 = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints(
|
|
[
|
|
Constraint::Length(1), // Title
|
|
Constraint::Length(3), // local explorer bg
|
|
Constraint::Length(3), // local explorer fg
|
|
Constraint::Length(3), // local explorer hg
|
|
Constraint::Length(3), // remote explorer bg
|
|
Constraint::Length(3), // remote explorer fg
|
|
Constraint::Length(3), // remote explorer hg
|
|
Constraint::Length(1), // Prevent overflow
|
|
]
|
|
.as_ref(),
|
|
)
|
|
.split(colors_layout[2]);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::TransferTitle),
|
|
f,
|
|
transfer_colors_layout_col1[0],
|
|
);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::ExplorerLocalBg),
|
|
f,
|
|
transfer_colors_layout_col1[1],
|
|
);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::ExplorerLocalFg),
|
|
f,
|
|
transfer_colors_layout_col1[2],
|
|
);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::ExplorerLocalHg),
|
|
f,
|
|
transfer_colors_layout_col1[3],
|
|
);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::ExplorerRemoteBg),
|
|
f,
|
|
transfer_colors_layout_col1[4],
|
|
);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::ExplorerRemoteFg),
|
|
f,
|
|
transfer_colors_layout_col1[5],
|
|
);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::ExplorerRemoteHg),
|
|
f,
|
|
transfer_colors_layout_col1[6],
|
|
);
|
|
let transfer_colors_layout_col2 = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.constraints(
|
|
[
|
|
Constraint::Length(1), // Title
|
|
Constraint::Length(3), // prog bar
|
|
Constraint::Length(3), // log bg
|
|
Constraint::Length(3), // log window
|
|
Constraint::Length(3), // status sorting
|
|
Constraint::Length(3), // status hidden
|
|
Constraint::Length(3), // sync browsing
|
|
Constraint::Length(1), // Prevent overflow
|
|
]
|
|
.as_ref(),
|
|
)
|
|
.split(colors_layout[3]);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::TransferTitle2),
|
|
f,
|
|
transfer_colors_layout_col2[0],
|
|
);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::ProgBar),
|
|
f,
|
|
transfer_colors_layout_col2[1],
|
|
);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::LogBg),
|
|
f,
|
|
transfer_colors_layout_col2[2],
|
|
);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::LogWindow),
|
|
f,
|
|
transfer_colors_layout_col2[3],
|
|
);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::StatusSorting),
|
|
f,
|
|
transfer_colors_layout_col2[4],
|
|
);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::StatusHidden),
|
|
f,
|
|
transfer_colors_layout_col2[5],
|
|
);
|
|
self.app.view(
|
|
&Id::Theme(IdTheme::StatusSync),
|
|
f,
|
|
transfer_colors_layout_col2[6],
|
|
);
|
|
// Popups
|
|
self.view_popups(f);
|
|
});
|
|
// Put context back to context
|
|
self.context = Some(ctx);
|
|
}
|
|
|
|
fn load_titles(&mut self) {
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::AuthTitle),
|
|
Box::<components::AuthTitle>::default(),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::MiscTitle),
|
|
Box::<components::MiscTitle>::default(),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::TransferTitle),
|
|
Box::<components::TransferTitle>::default(),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::TransferTitle2),
|
|
Box::<components::TransferTitle2>::default(),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
}
|
|
|
|
/// Load values from theme into input fields
|
|
pub(crate) fn load_styles(&mut self) {
|
|
let theme: Theme = self.theme().clone();
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::AuthAddress),
|
|
Box::new(components::AuthAddress::new(theme.auth_address)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::AuthBookmarks),
|
|
Box::new(components::AuthBookmarks::new(theme.auth_bookmarks)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::AuthPassword),
|
|
Box::new(components::AuthPassword::new(theme.auth_password)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::AuthPort),
|
|
Box::new(components::AuthPort::new(theme.auth_port)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::AuthProtocol),
|
|
Box::new(components::AuthProtocol::new(theme.auth_protocol)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::AuthRecentHosts),
|
|
Box::new(components::AuthRecentHosts::new(theme.auth_recents)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::AuthUsername),
|
|
Box::new(components::AuthUsername::new(theme.auth_username)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::MiscError),
|
|
Box::new(components::MiscError::new(theme.misc_error_dialog)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::MiscInfo),
|
|
Box::new(components::MiscInfo::new(theme.misc_info_dialog)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::MiscInput),
|
|
Box::new(components::MiscInput::new(theme.misc_input_dialog)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::MiscKeys),
|
|
Box::new(components::MiscKeys::new(theme.misc_keys)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::MiscQuit),
|
|
Box::new(components::MiscQuit::new(theme.misc_quit_dialog)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::MiscSave),
|
|
Box::new(components::MiscSave::new(theme.misc_save_dialog)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::MiscWarn),
|
|
Box::new(components::MiscWarn::new(theme.misc_warn_dialog)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::ExplorerLocalBg),
|
|
Box::new(components::ExplorerLocalBg::new(
|
|
theme.transfer_local_explorer_background,
|
|
)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::ExplorerLocalFg),
|
|
Box::new(components::ExplorerLocalFg::new(
|
|
theme.transfer_local_explorer_foreground,
|
|
)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::ExplorerLocalHg),
|
|
Box::new(components::ExplorerLocalHg::new(
|
|
theme.transfer_local_explorer_highlighted,
|
|
)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::ExplorerRemoteBg),
|
|
Box::new(components::ExplorerRemoteBg::new(
|
|
theme.transfer_remote_explorer_background,
|
|
)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::ExplorerRemoteFg),
|
|
Box::new(components::ExplorerRemoteFg::new(
|
|
theme.transfer_remote_explorer_foreground,
|
|
)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::ExplorerRemoteHg),
|
|
Box::new(components::ExplorerRemoteHg::new(
|
|
theme.transfer_remote_explorer_highlighted,
|
|
)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::ProgBar),
|
|
Box::new(components::ProgBar::new(theme.transfer_progress_bar)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::LogBg),
|
|
Box::new(components::LogBg::new(theme.transfer_log_background)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::LogWindow),
|
|
Box::new(components::LogWindow::new(theme.transfer_log_window)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::StatusSorting),
|
|
Box::new(components::StatusSorting::new(
|
|
theme.transfer_status_sorting,
|
|
)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::StatusHidden),
|
|
Box::new(components::StatusHidden::new(theme.transfer_status_hidden)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
if let Err(err) = self.app.remount(
|
|
Id::Theme(IdTheme::StatusSync),
|
|
Box::new(components::StatusSync::new(
|
|
theme.transfer_status_sync_browsing,
|
|
)),
|
|
vec![],
|
|
) {
|
|
error!("Failed to remount component: {err}");
|
|
}
|
|
}
|
|
}
|