Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ members = [

[workspace.package]
edition = "2024"
version = "0.1.2"
version = "0.1.3"
license = "AGPL-3"
authors = ["iamazy <iamazy.me@outlook.com>"]
documentation = "https://github.com/iamazy/termua"
Expand Down
1 change: 1 addition & 0 deletions crates/gpui_sftp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ gpui_common.workspace = true
gpui_transfer = { path = "../gpui_transfer" }
home.workspace = true
log.workspace = true
rust-i18n.workspace = true
smol.workspace = true
time.workspace = true

Expand Down
2 changes: 2 additions & 0 deletions crates/gpui_sftp/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use gpui::actions;

rust_i18n::i18n!("../../locales");

mod preview;
mod state;
mod view;
Expand Down
75 changes: 40 additions & 35 deletions crates/gpui_sftp/src/view/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@ use std::{
Arc,
atomic::{AtomicBool, AtomicUsize, Ordering},
},
time::{Duration, Instant},
};

use camino::Utf8PathBuf;
use gpui::{
App, AppContext, Bounds, Context, Div, Entity, EventEmitter, ExternalPaths, FocusHandle,
Focusable, Image, ImageSource, InteractiveElement, IntoElement, MouseButton, MouseDownEvent,
ParentElement, PathPromptOptions, PromptLevel, Render, SharedString, Stateful, Styled,
StyledImage, Subscription, Window, canvas, div, img, prelude::FluentBuilder, px,
MouseMoveEvent, MouseUpEvent, ParentElement, PathPromptOptions, PromptLevel, Render,
SharedString, Stateful, Styled, StyledImage, Subscription, Window, canvas, div, img,
prelude::FluentBuilder, px,
};
use gpui_common::TermuaIcon;
use gpui_component::{
Expand All @@ -33,6 +33,7 @@ use gpui_transfer::{
TransferTask,
};
use log::warn;
use rust_i18n::t;
use smol::{
Timer,
io::{AsyncReadExt, AsyncWriteExt},
Expand Down Expand Up @@ -113,8 +114,10 @@ enum ContextMenuTarget {

fn delete_selected_item_title(name: Option<&str>) -> String {
match name {
Some(name) if !name.trim().is_empty() => format!("Delete \"{name}\"?"),
_ => "Delete selected item?".to_string(),
Some(name) if !name.trim().is_empty() => {
t!("Sftp.Dialog.DeleteNamedItem", name = name).to_string()
}
_ => t!("Sftp.Dialog.DeleteSelectedItem").to_string(),
}
}

Expand Down Expand Up @@ -158,15 +161,17 @@ fn sftp_context_menu(target: ContextMenuTarget) -> Vec<ContextMenu> {
fn sftp_table_columns() -> Vec<Column> {
// Keys are stable identifiers used by Table; keep them short and ASCII.
vec![
Column::new("name", "Name").width(px(360.0)).resizable(true),
Column::new("size", "Size")
Column::new("name", t!("Sftp.Table.Name").to_string())
.width(px(360.0))
.resizable(true),
Column::new("size", t!("Sftp.Table.Size").to_string())
.width(px(120.0))
.text_right()
.resizable(true),
Column::new("modified", "Modified")
Column::new("modified", t!("Sftp.Table.Modified").to_string())
.width(px(180.0))
.resizable(true),
Column::new("perms", "Perms")
Column::new("perms", t!("Sftp.Table.Perms").to_string())
.width(px(120.0))
.resizable(true),
]
Expand Down Expand Up @@ -219,6 +224,7 @@ struct SftpTable {

selected_ids: HashSet<String>,
selection_anchor_id: Option<String>,
drag_selecting: bool,

columns: Vec<Column>,
sort: SortSpec,
Expand All @@ -236,7 +242,6 @@ pub struct SftpView {
path_input: Entity<InputState>,
path_editing: bool,
table_bounds: Bounds<gpui::Pixels>,
last_row_activate: Option<(usize, Instant)>,
preview: PreviewPane,
preview_epoch: usize,
show_preview: bool,
Expand Down Expand Up @@ -275,6 +280,24 @@ where
cx.new(|cx| configure(InputState::new(window, cx).placeholder(placeholder)))
}

fn table_event_activation_row(ev: &TableEvent) -> Option<usize> {
match ev {
TableEvent::DoubleClickedRow(row_ix) | TableEvent::DoubleClickedCell(row_ix, _) => {
Some(*row_ix)
}
_ => None,
}
}

fn begin_blank_table_drag_select(
state: &mut TableState<SftpTable>,
cx: &mut Context<TableState<SftpTable>>,
) {
state.clear_selection(cx);
state.delegate_mut().begin_blank_drag_select();
cx.notify();
}

impl SftpView {
pub fn new(sftp: wezterm_ssh::Sftp, window: &mut Window, cx: &mut Context<Self>) -> Self {
let focus_handle = cx.focus_handle();
Expand All @@ -284,7 +307,7 @@ impl SftpView {
.col_selectable(false)
.col_movable(false)
});
let path_input = new_input(window, cx, "Path");
let path_input = new_input(window, cx, t!("Sftp.Placeholder.Path").to_string());

// Bootstrap the root listing.
table.update(cx, |state, cx| {
Expand All @@ -299,7 +322,6 @@ impl SftpView {
path_input,
path_editing: false,
table_bounds: Bounds::default(),
last_row_activate: None,
preview: PreviewPane {
target: None,
content: PreviewContent::Empty,
Expand Down Expand Up @@ -380,16 +402,11 @@ impl SftpView {
ev: &TableEvent,
cx: &mut Context<Self>,
) {
// Prefer Table's native double-click events, but also provide a fallback path:
// on some platforms/backends, click_count can be unreliable, so we treat a fast
// repeated SelectRow on the same row as an "activate".
let mut activate_row: Option<usize> = None;
let activate_row = table_event_activation_row(ev);

match ev {
TableEvent::DoubleClickedRow(row_ix) => activate_row = Some(*row_ix),
TableEvent::DoubleClickedCell(row_ix, _col_ix) => activate_row = Some(*row_ix),
TableEvent::SelectRow(row_ix) => {
self.handle_table_row_selected(table, *row_ix, &mut activate_row, cx);
self.handle_table_row_selected(table, *row_ix, cx);
}
TableEvent::ClearSelection => self.close_preview(cx),
_ => {}
Expand All @@ -413,7 +430,6 @@ impl SftpView {
&mut self,
table: &Entity<TableState<SftpTable>>,
row_ix: usize,
activate_row: &mut Option<usize>,
cx: &mut Context<Self>,
) {
let target = table
Expand All @@ -439,18 +455,6 @@ impl SftpView {
}
PreviewGate::Hidden | PreviewGate::TooLarge { .. } => self.close_preview(cx),
}

let now = Instant::now();
// Use a short threshold similar to typical OS double-click timing.
let threshold = Duration::from_millis(450);
let is_fast_repeat = self
.last_row_activate
.map(|(prev_ix, prev_at)| prev_ix == row_ix && now.duration_since(prev_at) <= threshold)
.unwrap_or(false);
self.last_row_activate = Some((row_ix, now));
if is_fast_repeat {
*activate_row = Some(row_ix);
}
}

pub fn disconnect(&mut self, cx: &mut Context<Self>) {
Expand Down Expand Up @@ -537,16 +541,17 @@ impl SftpView {
let title = if targets_count <= 1 {
delete_selected_item_title(selected_name)
} else {
format!("Delete {targets_count} items?")
t!("Sftp.Dialog.DeleteItems", count = targets_count).to_string()
};
dialog
.title(title)
.confirm()
.button_props(
DialogButtonProps::default()
.ok_text("Delete")
.ok_text(t!("Sftp.Dialog.DeleteOk").to_string())
.ok_variant(ButtonVariant::Danger)
.cancel_text("Cancel"),
.cancel_text(t!("Sftp.Dialog.Cancel").to_string())
.show_cancel(true),
)
.on_ok(move |_e, window, cx| {
table.update(cx, |state, cx| {
Expand Down
16 changes: 13 additions & 3 deletions crates/gpui_sftp/src/view/preview.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use rust_i18n::t;

use super::{format::fenced_text_as_markdown, *};
use crate::preview::{PreviewGate, PreviewKind, gate_preview, read_bytes_with_limit};

Expand Down Expand Up @@ -62,14 +64,22 @@ impl SftpView {
let mut remote_f = match sftp.open(&target.id).await {
Ok(f) => f,
Err(err) => {
return PreviewOutcome::Error(format!("Open failed: {err}").into());
return PreviewOutcome::Error(
t!("Sftp.Preview.OpenFailed", err = err.to_string())
.to_string()
.into(),
);
}
};

let (bytes, truncated) = match read_bytes_with_limit(&mut remote_f, limit_bytes).await {
Ok(res) => res,
Err(err) => {
return PreviewOutcome::Error(format!("Read failed: {err}").into());
return PreviewOutcome::Error(
t!("Sftp.Preview.ReadFailed", err = err.to_string())
.to_string()
.into(),
);
}
};

Expand Down Expand Up @@ -129,7 +139,7 @@ impl SftpView {
cx.spawn(async move |this, cx| {
let outcome = match sftp {
Some(sftp) => load_preview_outcome(sftp, &target, kind, limit_bytes).await,
None => PreviewOutcome::Error("Disconnected".into()),
None => PreviewOutcome::Error(t!("Sftp.Toast.Disconnected").to_string().into()),
};

let _ = this.update(cx, |this, cx| {
Expand Down
Loading
Loading