mirror of
https://github.com/extrawurst/gitui.git
synced 2024-11-22 11:03:25 +03:00
Remotes popup (#2350)
Co-authored-by: extrawurst <mail@rusticorn.com> Co-authored-by: extrawurst <776816+extrawurst@users.noreply.github.com>
This commit is contained in:
parent
2cbeeeda95
commit
d4f9400e04
@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Fixes
|
||||
* respect env vars like `GIT_CONFIG_GLOBAL` ([#2298](https://github.com/extrawurst/gitui/issues/2298))
|
||||
|
||||
### Added
|
||||
* add popups for viewing, adding, updating and removing remotes [[@robin-thoene](https://github.com/robin-thoene)] ([#2172](https://github.com/extrawurst/gitui/issues/2172))
|
||||
|
||||
## [0.26.3] - 2024-06-02
|
||||
|
||||
### Breaking Changes
|
||||
|
@ -37,7 +37,7 @@ log = "0.4"
|
||||
notify = "6.1"
|
||||
notify-debouncer-mini = "0.4"
|
||||
once_cell = "1"
|
||||
# pin until upgrading this does not introduce a duplicte dependency
|
||||
# pin until upgrading this does not introduce a duplicate dependency
|
||||
parking_lot_core = "=0.9.9"
|
||||
ratatui = { version = "0.28", default-features = false, features = [
|
||||
'crossterm',
|
||||
|
@ -79,9 +79,10 @@ pub use merge::{
|
||||
};
|
||||
pub use rebase::rebase_branch;
|
||||
pub use remotes::{
|
||||
get_default_remote, get_default_remote_for_fetch,
|
||||
get_default_remote_for_push, get_remotes, push::AsyncProgress,
|
||||
tags::PushTagsProgress,
|
||||
add_remote, delete_remote, get_default_remote,
|
||||
get_default_remote_for_fetch, get_default_remote_for_push,
|
||||
get_remote_url, get_remotes, push::AsyncProgress, rename_remote,
|
||||
tags::PushTagsProgress, update_remote_url, validate_remote_name,
|
||||
};
|
||||
pub(crate) use repository::repo;
|
||||
pub use repository::{RepoPath, RepoPathRef};
|
||||
|
@ -13,7 +13,9 @@ use crate::{
|
||||
ProgressPercent,
|
||||
};
|
||||
use crossbeam_channel::Sender;
|
||||
use git2::{BranchType, FetchOptions, ProxyOptions, Repository};
|
||||
use git2::{
|
||||
BranchType, FetchOptions, ProxyOptions, Remote, Repository,
|
||||
};
|
||||
use scopetime::scope_time;
|
||||
use utils::bytes2string;
|
||||
|
||||
@ -32,6 +34,54 @@ pub fn proxy_auto<'a>() -> ProxyOptions<'a> {
|
||||
proxy
|
||||
}
|
||||
|
||||
///
|
||||
pub fn add_remote(
|
||||
repo_path: &RepoPath,
|
||||
name: &str,
|
||||
url: &str,
|
||||
) -> Result<()> {
|
||||
let repo = repo(repo_path)?;
|
||||
repo.remote(name, url)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn rename_remote(
|
||||
repo_path: &RepoPath,
|
||||
name: &str,
|
||||
new_name: &str,
|
||||
) -> Result<()> {
|
||||
let repo = repo(repo_path)?;
|
||||
repo.remote_rename(name, new_name)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn update_remote_url(
|
||||
repo_path: &RepoPath,
|
||||
name: &str,
|
||||
new_url: &str,
|
||||
) -> Result<()> {
|
||||
let repo = repo(repo_path)?;
|
||||
repo.remote_set_url(name, new_url)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn delete_remote(
|
||||
repo_path: &RepoPath,
|
||||
remote_name: &str,
|
||||
) -> Result<()> {
|
||||
let repo = repo(repo_path)?;
|
||||
repo.remote_delete(remote_name)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn validate_remote_name(name: &str) -> bool {
|
||||
Remote::is_valid_name(name)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_remotes(repo_path: &RepoPath) -> Result<Vec<String>> {
|
||||
scope_time!("get_remotes");
|
||||
@ -44,6 +94,20 @@ pub fn get_remotes(repo_path: &RepoPath) -> Result<Vec<String>> {
|
||||
Ok(remotes)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn get_remote_url(
|
||||
repo_path: &RepoPath,
|
||||
remote_name: &str,
|
||||
) -> Result<Option<String>> {
|
||||
let repo = repo(repo_path)?;
|
||||
let remote = repo.find_remote(remote_name)?.clone();
|
||||
let url = remote.url();
|
||||
if let Some(u) = url {
|
||||
return Ok(Some(u.to_string()));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// tries to find origin or the only remote that is defined if any
|
||||
/// in case of multiple remotes and none named *origin* we fail
|
||||
pub fn get_default_remote(repo_path: &RepoPath) -> Result<String> {
|
||||
|
68
src/app.rs
68
src/app.rs
@ -12,12 +12,14 @@ use crate::{
|
||||
popups::{
|
||||
AppOption, BlameFilePopup, BranchListPopup, CommitPopup,
|
||||
CompareCommitsPopup, ConfirmPopup, CreateBranchPopup,
|
||||
ExternalEditorPopup, FetchPopup, FileRevlogPopup,
|
||||
FuzzyFindPopup, HelpPopup, InspectCommitPopup,
|
||||
LogSearchPopupPopup, MsgPopup, OptionsPopup, PullPopup,
|
||||
PushPopup, PushTagsPopup, RenameBranchPopup, ResetPopup,
|
||||
RevisionFilesPopup, StashMsgPopup, SubmodulesListPopup,
|
||||
TagCommitPopup, TagListPopup,
|
||||
CreateRemotePopup, ExternalEditorPopup, FetchPopup,
|
||||
FileRevlogPopup, FuzzyFindPopup, HelpPopup,
|
||||
InspectCommitPopup, LogSearchPopupPopup, MsgPopup,
|
||||
OptionsPopup, PullPopup, PushPopup, PushTagsPopup,
|
||||
RemoteListPopup, RenameBranchPopup, RenameRemotePopup,
|
||||
ResetPopup, RevisionFilesPopup, StashMsgPopup,
|
||||
SubmodulesListPopup, TagCommitPopup, TagListPopup,
|
||||
UpdateRemoteUrlPopup,
|
||||
},
|
||||
queue::{
|
||||
Action, AppTabs, InternalEvent, NeedsUpdate, Queue,
|
||||
@ -86,6 +88,10 @@ pub struct App {
|
||||
fetch_popup: FetchPopup,
|
||||
tag_commit_popup: TagCommitPopup,
|
||||
create_branch_popup: CreateBranchPopup,
|
||||
create_remote_popup: CreateRemotePopup,
|
||||
rename_remote_popup: RenameRemotePopup,
|
||||
update_remote_url_popup: UpdateRemoteUrlPopup,
|
||||
remotes_popup: RemoteListPopup,
|
||||
rename_branch_popup: RenameBranchPopup,
|
||||
select_branch_popup: BranchListPopup,
|
||||
options_popup: OptionsPopup,
|
||||
@ -189,6 +195,10 @@ impl App {
|
||||
fetch_popup: FetchPopup::new(&env),
|
||||
tag_commit_popup: TagCommitPopup::new(&env),
|
||||
create_branch_popup: CreateBranchPopup::new(&env),
|
||||
create_remote_popup: CreateRemotePopup::new(&env),
|
||||
rename_remote_popup: RenameRemotePopup::new(&env),
|
||||
update_remote_url_popup: UpdateRemoteUrlPopup::new(&env),
|
||||
remotes_popup: RemoteListPopup::new(&env),
|
||||
rename_branch_popup: RenameBranchPopup::new(&env),
|
||||
select_branch_popup: BranchListPopup::new(&env),
|
||||
tags_popup: TagListPopup::new(&env),
|
||||
@ -484,6 +494,10 @@ impl App {
|
||||
tag_commit_popup,
|
||||
reset_popup,
|
||||
create_branch_popup,
|
||||
create_remote_popup,
|
||||
rename_remote_popup,
|
||||
update_remote_url_popup,
|
||||
remotes_popup,
|
||||
rename_branch_popup,
|
||||
select_branch_popup,
|
||||
revision_files_popup,
|
||||
@ -512,6 +526,10 @@ impl App {
|
||||
external_editor_popup,
|
||||
tag_commit_popup,
|
||||
select_branch_popup,
|
||||
remotes_popup,
|
||||
create_remote_popup,
|
||||
rename_remote_popup,
|
||||
update_remote_url_popup,
|
||||
submodule_popup,
|
||||
tags_popup,
|
||||
reset_popup,
|
||||
@ -646,6 +664,9 @@ impl App {
|
||||
if flags.contains(NeedsUpdate::BRANCHES) {
|
||||
self.select_branch_popup.update_branches()?;
|
||||
}
|
||||
if flags.contains(NeedsUpdate::REMOTES) {
|
||||
self.remotes_popup.update_remotes()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -727,7 +748,19 @@ impl App {
|
||||
InternalEvent::TagCommit(id) => {
|
||||
self.tag_commit_popup.open(id)?;
|
||||
}
|
||||
|
||||
InternalEvent::CreateRemote => {
|
||||
self.create_remote_popup.open()?;
|
||||
}
|
||||
InternalEvent::RenameRemote(cur_name) => {
|
||||
self.rename_remote_popup.open(cur_name)?;
|
||||
}
|
||||
InternalEvent::UpdateRemoteUrl(remote_name, cur_url) => {
|
||||
self.update_remote_url_popup
|
||||
.open(remote_name, cur_url)?;
|
||||
}
|
||||
InternalEvent::ViewRemotes => {
|
||||
self.remotes_popup.open()?;
|
||||
}
|
||||
InternalEvent::CreateBranch => {
|
||||
self.create_branch_popup.open()?;
|
||||
}
|
||||
@ -926,6 +959,9 @@ impl App {
|
||||
Action::DeleteRemoteBranch(branch_ref) => {
|
||||
self.delete_remote_branch(&branch_ref)?;
|
||||
}
|
||||
Action::DeleteRemote(remote_name) => {
|
||||
self.delete_remote(&remote_name);
|
||||
}
|
||||
Action::DeleteTag(tag_name) => {
|
||||
self.delete_tag(tag_name)?;
|
||||
}
|
||||
@ -1015,6 +1051,24 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_remote(&self, remote_name: &str) {
|
||||
let res =
|
||||
sync::delete_remote(&self.repo.borrow(), remote_name);
|
||||
match res {
|
||||
Ok(()) => {
|
||||
self.queue.push(InternalEvent::Update(
|
||||
NeedsUpdate::ALL | NeedsUpdate::REMOTES,
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("delete remote: {}", e,);
|
||||
self.queue.push(InternalEvent::ShowErrorMsg(
|
||||
format!("delete remote error:\n{e}",),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn commands(&self, force_all: bool) -> Vec<CommandInfo> {
|
||||
let mut res = Vec::new();
|
||||
|
||||
|
@ -118,6 +118,11 @@ pub struct KeysList {
|
||||
pub stage_unstage_item: GituiKeyEvent,
|
||||
pub tag_annotate: GituiKeyEvent,
|
||||
pub view_submodules: GituiKeyEvent,
|
||||
pub view_remotes: GituiKeyEvent,
|
||||
pub update_remote_name: GituiKeyEvent,
|
||||
pub update_remote_url: GituiKeyEvent,
|
||||
pub add_remote: GituiKeyEvent,
|
||||
pub delete_remote: GituiKeyEvent,
|
||||
pub view_submodule_parent: GituiKeyEvent,
|
||||
pub update_submodule: GituiKeyEvent,
|
||||
pub commit_history_next: GituiKeyEvent,
|
||||
@ -210,6 +215,11 @@ impl Default for KeysList {
|
||||
stage_unstage_item: GituiKeyEvent::new(KeyCode::Enter, KeyModifiers::empty()),
|
||||
tag_annotate: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::CONTROL),
|
||||
view_submodules: GituiKeyEvent::new(KeyCode::Char('S'), KeyModifiers::SHIFT),
|
||||
view_remotes: GituiKeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL),
|
||||
update_remote_name: GituiKeyEvent::new(KeyCode::Char('n'),KeyModifiers::NONE),
|
||||
update_remote_url: GituiKeyEvent::new(KeyCode::Char('u'),KeyModifiers::NONE),
|
||||
add_remote: GituiKeyEvent::new(KeyCode::Char('a'), KeyModifiers::NONE),
|
||||
delete_remote: GituiKeyEvent::new(KeyCode::Char('r'), KeyModifiers::NONE),
|
||||
view_submodule_parent: GituiKeyEvent::new(KeyCode::Char('p'), KeyModifiers::empty()),
|
||||
update_submodule: GituiKeyEvent::new(KeyCode::Char('u'), KeyModifiers::empty()),
|
||||
commit_history_next: GituiKeyEvent::new(KeyCode::Char('n'), KeyModifiers::CONTROL),
|
||||
|
@ -112,111 +112,7 @@ impl Component for BranchListPopup {
|
||||
out.clear();
|
||||
}
|
||||
|
||||
let selection_is_cur_branch =
|
||||
self.selection_is_cur_branch();
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::scroll(&self.key_config),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::close_popup(&self.key_config),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::commit_details_open(
|
||||
&self.key_config,
|
||||
),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::compare_with_head(
|
||||
&self.key_config,
|
||||
),
|
||||
!selection_is_cur_branch,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::toggle_branch_popup(
|
||||
&self.key_config,
|
||||
self.local,
|
||||
),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::select_branch_popup(
|
||||
&self.key_config,
|
||||
),
|
||||
!selection_is_cur_branch && self.valid_selection(),
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::open_branch_create_popup(
|
||||
&self.key_config,
|
||||
),
|
||||
true,
|
||||
self.local,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::delete_branch_popup(
|
||||
&self.key_config,
|
||||
),
|
||||
!selection_is_cur_branch,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::merge_branch_popup(
|
||||
&self.key_config,
|
||||
),
|
||||
!selection_is_cur_branch,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::branch_popup_rebase(
|
||||
&self.key_config,
|
||||
),
|
||||
!selection_is_cur_branch,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::rename_branch_popup(
|
||||
&self.key_config,
|
||||
),
|
||||
true,
|
||||
self.local,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::fetch_remotes(&self.key_config),
|
||||
self.has_remotes,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::find_branch(&self.key_config),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::reset_branch(&self.key_config),
|
||||
self.valid_selection(),
|
||||
true,
|
||||
));
|
||||
self.add_commands_internal(out);
|
||||
}
|
||||
visibility_blocking(self)
|
||||
}
|
||||
@ -294,6 +190,9 @@ impl Component for BranchListPopup {
|
||||
&& self.has_remotes
|
||||
{
|
||||
self.queue.push(InternalEvent::FetchRemotes);
|
||||
} else if key_match(e, self.key_config.keys.view_remotes)
|
||||
{
|
||||
self.queue.push(InternalEvent::ViewRemotes);
|
||||
} else if key_match(e, self.key_config.keys.reset_branch)
|
||||
{
|
||||
if let Some(commit_id) = self.get_selected_commit() {
|
||||
@ -776,4 +675,103 @@ impl BranchListPopup {
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
fn add_commands_internal(&self, out: &mut Vec<CommandInfo>) {
|
||||
let selection_is_cur_branch = self.selection_is_cur_branch();
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::scroll(&self.key_config),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::close_popup(&self.key_config),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::commit_details_open(&self.key_config),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::compare_with_head(&self.key_config),
|
||||
!selection_is_cur_branch,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::toggle_branch_popup(
|
||||
&self.key_config,
|
||||
self.local,
|
||||
),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::select_branch_popup(&self.key_config),
|
||||
!selection_is_cur_branch && self.valid_selection(),
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::open_branch_create_popup(
|
||||
&self.key_config,
|
||||
),
|
||||
true,
|
||||
self.local,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::delete_branch_popup(&self.key_config),
|
||||
!selection_is_cur_branch,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::merge_branch_popup(&self.key_config),
|
||||
!selection_is_cur_branch,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::branch_popup_rebase(&self.key_config),
|
||||
!selection_is_cur_branch,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::rename_branch_popup(&self.key_config),
|
||||
true,
|
||||
self.local,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::fetch_remotes(&self.key_config),
|
||||
self.has_remotes,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::find_branch(&self.key_config),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::reset_branch(&self.key_config),
|
||||
self.valid_selection(),
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::view_remotes(&self.key_config),
|
||||
true,
|
||||
self.has_remotes,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
@ -168,6 +168,10 @@ impl ConfirmPopup {
|
||||
branch_ref,
|
||||
),
|
||||
),
|
||||
Action::DeleteRemote(remote_name)=>(
|
||||
strings::confirm_title_delete_remote(&self.key_config),
|
||||
strings::confirm_msg_delete_remote(&self.key_config,remote_name),
|
||||
),
|
||||
Action::DeleteTag(tag_name) => (
|
||||
strings::confirm_title_delete_tag(
|
||||
&self.key_config,
|
||||
|
211
src/popups/create_remote.rs
Normal file
211
src/popups/create_remote.rs
Normal file
@ -0,0 +1,211 @@
|
||||
use anyhow::Result;
|
||||
use asyncgit::sync::{self, validate_remote_name, RepoPathRef};
|
||||
use crossterm::event::Event;
|
||||
use easy_cast::Cast;
|
||||
use ratatui::{widgets::Paragraph, Frame};
|
||||
|
||||
use crate::{
|
||||
app::Environment,
|
||||
components::{
|
||||
visibility_blocking, CommandBlocking, CommandInfo, Component,
|
||||
DrawableComponent, EventState, InputType, TextInputComponent,
|
||||
},
|
||||
keys::{key_match, SharedKeyConfig},
|
||||
queue::{InternalEvent, NeedsUpdate, Queue},
|
||||
strings,
|
||||
ui::style::SharedTheme,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
enum State {
|
||||
#[default]
|
||||
Name,
|
||||
Url {
|
||||
name: String,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct CreateRemotePopup {
|
||||
repo: RepoPathRef,
|
||||
input: TextInputComponent,
|
||||
queue: Queue,
|
||||
key_config: SharedKeyConfig,
|
||||
state: State,
|
||||
theme: SharedTheme,
|
||||
}
|
||||
|
||||
impl DrawableComponent for CreateRemotePopup {
|
||||
fn draw(
|
||||
&self,
|
||||
f: &mut ratatui::Frame,
|
||||
rect: ratatui::prelude::Rect,
|
||||
) -> anyhow::Result<()> {
|
||||
if self.is_visible() {
|
||||
self.input.draw(f, rect)?;
|
||||
self.draw_warnings(f);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for CreateRemotePopup {
|
||||
fn commands(
|
||||
&self,
|
||||
out: &mut Vec<CommandInfo>,
|
||||
force_all: bool,
|
||||
) -> CommandBlocking {
|
||||
if self.is_visible() || force_all {
|
||||
self.input.commands(out, force_all);
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::remote_confirm_name_msg(
|
||||
&self.key_config,
|
||||
),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
}
|
||||
visibility_blocking(self)
|
||||
}
|
||||
|
||||
fn event(
|
||||
&mut self,
|
||||
ev: &crossterm::event::Event,
|
||||
) -> Result<EventState> {
|
||||
if self.is_visible() {
|
||||
if self.input.event(ev)?.is_consumed() {
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
|
||||
if let Event::Key(e) = ev {
|
||||
if key_match(e, self.key_config.keys.enter) {
|
||||
self.handle_submit();
|
||||
}
|
||||
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
}
|
||||
Ok(EventState::NotConsumed)
|
||||
}
|
||||
|
||||
fn is_visible(&self) -> bool {
|
||||
self.input.is_visible()
|
||||
}
|
||||
|
||||
fn hide(&mut self) {
|
||||
self.input.hide();
|
||||
}
|
||||
|
||||
fn show(&mut self) -> Result<()> {
|
||||
self.input.clear();
|
||||
self.input.set_title(
|
||||
strings::create_remote_popup_title_name(&self.key_config),
|
||||
);
|
||||
self.input.set_default_msg(
|
||||
strings::create_remote_popup_msg_name(&self.key_config),
|
||||
);
|
||||
|
||||
self.input.show()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl CreateRemotePopup {
|
||||
pub fn new(env: &Environment) -> Self {
|
||||
Self {
|
||||
repo: env.repo.clone(),
|
||||
queue: env.queue.clone(),
|
||||
input: TextInputComponent::new(env, "", "", true)
|
||||
.with_input_type(InputType::Singleline),
|
||||
key_config: env.key_config.clone(),
|
||||
state: State::Name,
|
||||
theme: env.theme.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open(&mut self) -> Result<()> {
|
||||
self.state = State::Name;
|
||||
self.input.clear();
|
||||
self.show()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_warnings(&self, f: &mut Frame) {
|
||||
let remote_name = match self.state {
|
||||
State::Name => self.input.get_text(),
|
||||
State::Url { .. } => return,
|
||||
};
|
||||
|
||||
if !remote_name.is_empty() {
|
||||
let valid = validate_remote_name(remote_name);
|
||||
|
||||
if !valid {
|
||||
let msg = strings::remote_name_invalid();
|
||||
let msg_length: u16 = msg.len().cast();
|
||||
let w = Paragraph::new(msg)
|
||||
.style(self.theme.text_danger());
|
||||
|
||||
let rect = {
|
||||
let mut rect = self.input.get_area();
|
||||
rect.y += rect.height.saturating_sub(1);
|
||||
rect.height = 1;
|
||||
let offset =
|
||||
rect.width.saturating_sub(msg_length + 1);
|
||||
rect.width =
|
||||
rect.width.saturating_sub(offset + 1);
|
||||
rect.x += offset;
|
||||
|
||||
rect
|
||||
};
|
||||
|
||||
f.render_widget(w, rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_submit(&mut self) {
|
||||
match &self.state {
|
||||
State::Name => {
|
||||
self.input.clear();
|
||||
self.input.set_title(
|
||||
strings::create_remote_popup_title_url(
|
||||
&self.key_config,
|
||||
),
|
||||
);
|
||||
self.input.set_default_msg(
|
||||
strings::create_remote_popup_msg_url(
|
||||
&self.key_config,
|
||||
),
|
||||
);
|
||||
self.state = State::Url {
|
||||
name: self.input.get_text().to_string(),
|
||||
};
|
||||
}
|
||||
State::Url { name } => {
|
||||
let res = sync::add_remote(
|
||||
&self.repo.borrow(),
|
||||
name,
|
||||
self.input.get_text(),
|
||||
);
|
||||
|
||||
match res {
|
||||
Ok(()) => {
|
||||
self.queue.push(InternalEvent::Update(
|
||||
NeedsUpdate::ALL | NeedsUpdate::REMOTES,
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("create remote: {}", e,);
|
||||
self.queue.push(InternalEvent::ShowErrorMsg(
|
||||
format!("create remote error:\n{e}",),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
self.hide();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ mod commit;
|
||||
mod compare_commits;
|
||||
mod confirm;
|
||||
mod create_branch;
|
||||
mod create_remote;
|
||||
mod externaleditor;
|
||||
mod fetch;
|
||||
mod file_revlog;
|
||||
@ -16,13 +17,16 @@ mod options;
|
||||
mod pull;
|
||||
mod push;
|
||||
mod push_tags;
|
||||
mod remotelist;
|
||||
mod rename_branch;
|
||||
mod rename_remote;
|
||||
mod reset;
|
||||
mod revision_files;
|
||||
mod stashmsg;
|
||||
mod submodules;
|
||||
mod tag_commit;
|
||||
mod taglist;
|
||||
mod update_remote_url;
|
||||
|
||||
pub use blame_file::{BlameFileOpen, BlameFilePopup};
|
||||
pub use branchlist::BranchListPopup;
|
||||
@ -30,6 +34,7 @@ pub use commit::CommitPopup;
|
||||
pub use compare_commits::CompareCommitsPopup;
|
||||
pub use confirm::ConfirmPopup;
|
||||
pub use create_branch::CreateBranchPopup;
|
||||
pub use create_remote::CreateRemotePopup;
|
||||
pub use externaleditor::ExternalEditorPopup;
|
||||
pub use fetch::FetchPopup;
|
||||
pub use file_revlog::{FileRevOpen, FileRevlogPopup};
|
||||
@ -42,13 +47,16 @@ pub use options::{AppOption, OptionsPopup};
|
||||
pub use pull::PullPopup;
|
||||
pub use push::PushPopup;
|
||||
pub use push_tags::PushTagsPopup;
|
||||
pub use remotelist::RemoteListPopup;
|
||||
pub use rename_branch::RenameBranchPopup;
|
||||
pub use rename_remote::RenameRemotePopup;
|
||||
pub use reset::ResetPopup;
|
||||
pub use revision_files::{FileTreeOpen, RevisionFilesPopup};
|
||||
pub use stashmsg::StashMsgPopup;
|
||||
pub use submodules::SubmodulesListPopup;
|
||||
pub use tag_commit::TagCommitPopup;
|
||||
pub use taglist::TagListPopup;
|
||||
pub use update_remote_url::UpdateRemoteUrlPopup;
|
||||
|
||||
use crate::ui::style::Theme;
|
||||
use ratatui::{
|
||||
|
475
src/popups/remotelist.rs
Normal file
475
src/popups/remotelist.rs
Normal file
@ -0,0 +1,475 @@
|
||||
use std::cell::Cell;
|
||||
|
||||
use asyncgit::sync::{get_remote_url, get_remotes, RepoPathRef};
|
||||
use ratatui::{
|
||||
layout::{
|
||||
Alignment, Constraint, Direction, Layout, Margin, Rect,
|
||||
},
|
||||
text::{Line, Span, Text},
|
||||
widgets::{Block, BorderType, Borders, Clear, Paragraph, Wrap},
|
||||
Frame,
|
||||
};
|
||||
use unicode_truncate::UnicodeTruncateStr;
|
||||
|
||||
use crate::{
|
||||
app::Environment,
|
||||
components::{
|
||||
visibility_blocking, CommandBlocking, CommandInfo, Component,
|
||||
DrawableComponent, EventState, ScrollType, VerticalScroll,
|
||||
},
|
||||
keys::{key_match, SharedKeyConfig},
|
||||
queue::{Action, InternalEvent, Queue},
|
||||
strings,
|
||||
ui::{self, style::SharedTheme, Size},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use crossterm::event::{Event, KeyEvent};
|
||||
|
||||
pub struct RemoteListPopup {
|
||||
remote_names: Vec<String>,
|
||||
repo: RepoPathRef,
|
||||
visible: bool,
|
||||
current_height: Cell<u16>,
|
||||
queue: Queue,
|
||||
selection: u16,
|
||||
scroll: VerticalScroll,
|
||||
theme: SharedTheme,
|
||||
key_config: SharedKeyConfig,
|
||||
}
|
||||
|
||||
impl DrawableComponent for RemoteListPopup {
|
||||
fn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {
|
||||
if self.is_visible() {
|
||||
const PERCENT_SIZE: Size = Size::new(40, 30);
|
||||
const MIN_SIZE: Size = Size::new(30, 20);
|
||||
let area = ui::centered_rect(
|
||||
PERCENT_SIZE.width,
|
||||
PERCENT_SIZE.height,
|
||||
rect,
|
||||
);
|
||||
let area = ui::rect_inside(MIN_SIZE, rect.into(), area);
|
||||
let area = area.intersection(rect);
|
||||
f.render_widget(Clear, area);
|
||||
f.render_widget(
|
||||
Block::default()
|
||||
.title(strings::POPUP_TITLE_REMOTES)
|
||||
.border_type(BorderType::Thick)
|
||||
.borders(Borders::ALL),
|
||||
area,
|
||||
);
|
||||
let area = area.inner(Margin {
|
||||
vertical: 1,
|
||||
horizontal: 1,
|
||||
});
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![
|
||||
Constraint::Min(1),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(2),
|
||||
])
|
||||
.split(area);
|
||||
self.draw_remotes_list(f, chunks[0])?;
|
||||
self.draw_separator(f, chunks[1]);
|
||||
self.draw_selected_remote_details(f, chunks[2]);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for RemoteListPopup {
|
||||
fn commands(
|
||||
&self,
|
||||
out: &mut Vec<CommandInfo>,
|
||||
force_all: bool,
|
||||
) -> CommandBlocking {
|
||||
if self.is_visible() || force_all {
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::scroll(&self.key_config),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::close_popup(&self.key_config),
|
||||
true,
|
||||
self.is_visible(),
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::update_remote_name(
|
||||
&self.key_config,
|
||||
),
|
||||
true,
|
||||
self.valid_selection(),
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::update_remote_url(
|
||||
&self.key_config,
|
||||
),
|
||||
true,
|
||||
self.valid_selection(),
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::create_remote(&self.key_config),
|
||||
true,
|
||||
self.valid_selection(),
|
||||
));
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::delete_remote_popup(
|
||||
&self.key_config,
|
||||
),
|
||||
true,
|
||||
self.valid_selection(),
|
||||
));
|
||||
}
|
||||
visibility_blocking(self)
|
||||
}
|
||||
|
||||
fn event(&mut self, ev: &Event) -> Result<EventState> {
|
||||
if !self.visible {
|
||||
return Ok(EventState::NotConsumed);
|
||||
}
|
||||
|
||||
if let Event::Key(e) = ev {
|
||||
if self.move_event(e)?.is_consumed() {
|
||||
return Ok(EventState::Consumed);
|
||||
} else if key_match(e, self.key_config.keys.add_remote) {
|
||||
self.queue.push(InternalEvent::CreateRemote);
|
||||
} else if key_match(e, self.key_config.keys.delete_remote)
|
||||
&& self.valid_selection()
|
||||
{
|
||||
self.delete_remote();
|
||||
} else if key_match(
|
||||
e,
|
||||
self.key_config.keys.update_remote_name,
|
||||
) {
|
||||
self.rename_remote();
|
||||
} else if key_match(
|
||||
e,
|
||||
self.key_config.keys.update_remote_url,
|
||||
) {
|
||||
self.update_remote_url();
|
||||
}
|
||||
}
|
||||
Ok(EventState::Consumed)
|
||||
}
|
||||
|
||||
fn is_visible(&self) -> bool {
|
||||
self.visible
|
||||
}
|
||||
|
||||
fn hide(&mut self) {
|
||||
self.visible = false;
|
||||
}
|
||||
|
||||
fn show(&mut self) -> Result<()> {
|
||||
self.visible = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteListPopup {
|
||||
pub fn new(env: &Environment) -> Self {
|
||||
Self {
|
||||
remote_names: Vec::new(),
|
||||
repo: env.repo.clone(),
|
||||
visible: false,
|
||||
scroll: VerticalScroll::new(),
|
||||
theme: env.theme.clone(),
|
||||
key_config: env.key_config.clone(),
|
||||
queue: env.queue.clone(),
|
||||
current_height: Cell::new(0),
|
||||
selection: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn move_event(&mut self, e: &KeyEvent) -> Result<EventState> {
|
||||
if key_match(e, self.key_config.keys.exit_popup) {
|
||||
self.hide();
|
||||
} else if key_match(e, self.key_config.keys.move_down) {
|
||||
return self
|
||||
.move_selection(ScrollType::Up)
|
||||
.map(Into::into);
|
||||
} else if key_match(e, self.key_config.keys.move_up) {
|
||||
return self
|
||||
.move_selection(ScrollType::Down)
|
||||
.map(Into::into);
|
||||
} else if key_match(e, self.key_config.keys.page_down) {
|
||||
return self
|
||||
.move_selection(ScrollType::PageDown)
|
||||
.map(Into::into);
|
||||
} else if key_match(e, self.key_config.keys.page_up) {
|
||||
return self
|
||||
.move_selection(ScrollType::PageUp)
|
||||
.map(Into::into);
|
||||
} else if key_match(e, self.key_config.keys.home) {
|
||||
return self
|
||||
.move_selection(ScrollType::Home)
|
||||
.map(Into::into);
|
||||
} else if key_match(e, self.key_config.keys.end) {
|
||||
return self
|
||||
.move_selection(ScrollType::End)
|
||||
.map(Into::into);
|
||||
}
|
||||
Ok(EventState::NotConsumed)
|
||||
}
|
||||
|
||||
///
|
||||
pub fn open(&mut self) -> Result<()> {
|
||||
self.show()?;
|
||||
self.update_remotes()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_text(
|
||||
&self,
|
||||
theme: &SharedTheme,
|
||||
width_available: u16,
|
||||
height: usize,
|
||||
) -> Text {
|
||||
const THREE_DOTS: &str = "...";
|
||||
const THREE_DOTS_LENGTH: usize = THREE_DOTS.len(); // "..."
|
||||
|
||||
let name_length: usize = (width_available as usize)
|
||||
.saturating_sub(THREE_DOTS_LENGTH);
|
||||
|
||||
Text::from(
|
||||
self.remote_names
|
||||
.iter()
|
||||
.skip(self.scroll.get_top())
|
||||
.take(height)
|
||||
.enumerate()
|
||||
.map(|(i, remote)| {
|
||||
let selected = (self.selection as usize
|
||||
- self.scroll.get_top())
|
||||
== i;
|
||||
let mut remote_name = remote.clone();
|
||||
if remote_name.len()
|
||||
> name_length
|
||||
.saturating_sub(THREE_DOTS_LENGTH)
|
||||
{
|
||||
remote_name = remote_name
|
||||
.unicode_truncate(
|
||||
name_length.saturating_sub(
|
||||
THREE_DOTS_LENGTH,
|
||||
),
|
||||
)
|
||||
.0
|
||||
.to_string();
|
||||
remote_name += THREE_DOTS;
|
||||
}
|
||||
let span_name = Span::styled(
|
||||
format!("{remote_name:name_length$}"),
|
||||
theme.text(true, selected),
|
||||
);
|
||||
Line::from(vec![span_name])
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn draw_remotes_list(
|
||||
&self,
|
||||
f: &mut Frame,
|
||||
r: Rect,
|
||||
) -> Result<()> {
|
||||
let height_in_lines = r.height as usize;
|
||||
self.current_height.set(height_in_lines.try_into()?);
|
||||
|
||||
self.scroll.update(
|
||||
self.selection as usize,
|
||||
self.remote_names.len(),
|
||||
height_in_lines,
|
||||
);
|
||||
|
||||
f.render_widget(
|
||||
Paragraph::new(self.get_text(
|
||||
&self.theme,
|
||||
r.width.saturating_add(1),
|
||||
height_in_lines,
|
||||
))
|
||||
.alignment(Alignment::Left),
|
||||
r,
|
||||
);
|
||||
|
||||
let mut r = r;
|
||||
r.width += 1;
|
||||
r.height += 2;
|
||||
r.y = r.y.saturating_sub(1);
|
||||
|
||||
self.scroll.draw(f, r, &self.theme);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_separator(&self, f: &mut Frame, r: Rect) {
|
||||
// Discard self argument because it is not needed.
|
||||
let _ = self;
|
||||
f.render_widget(
|
||||
Block::default()
|
||||
.title(strings::POPUP_SUBTITLE_REMOTES)
|
||||
.border_type(BorderType::Plain)
|
||||
.borders(Borders::TOP),
|
||||
r,
|
||||
);
|
||||
}
|
||||
|
||||
fn draw_selected_remote_details(&self, f: &mut Frame, r: Rect) {
|
||||
const THREE_DOTS: &str = "...";
|
||||
const THREE_DOTS_LENGTH: usize = THREE_DOTS.len(); // "..."
|
||||
const REMOTE_NAME_LABEL: &str = "name: ";
|
||||
const REMOTE_NAME_LABEL_LENGTH: usize =
|
||||
REMOTE_NAME_LABEL.len();
|
||||
const REMOTE_URL_LABEL: &str = "url: ";
|
||||
const REMOTE_URL_LABEL_LENGTH: usize = REMOTE_URL_LABEL.len();
|
||||
|
||||
let name_length: usize = (r.width.saturating_sub(1) as usize)
|
||||
.saturating_sub(REMOTE_NAME_LABEL_LENGTH);
|
||||
let url_length: usize = (r.width.saturating_sub(1) as usize)
|
||||
.saturating_sub(REMOTE_URL_LABEL_LENGTH);
|
||||
|
||||
let remote =
|
||||
self.remote_names.get(usize::from(self.selection));
|
||||
if let Some(remote) = remote {
|
||||
let mut remote_name = remote.clone();
|
||||
if remote_name.len()
|
||||
> name_length.saturating_sub(THREE_DOTS_LENGTH)
|
||||
{
|
||||
remote_name = remote_name
|
||||
.unicode_truncate(
|
||||
name_length.saturating_sub(THREE_DOTS_LENGTH),
|
||||
)
|
||||
.0
|
||||
.to_string();
|
||||
remote_name += THREE_DOTS;
|
||||
}
|
||||
let mut lines = Vec::<Line>::new();
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(
|
||||
"{REMOTE_NAME_LABEL}{remote_name:name_length$}"
|
||||
),
|
||||
self.theme.text(true, false),
|
||||
)));
|
||||
let remote_url =
|
||||
get_remote_url(&self.repo.borrow(), remote);
|
||||
if let Ok(Some(mut remote_url)) = remote_url {
|
||||
if remote_url.len()
|
||||
> url_length.saturating_sub(THREE_DOTS_LENGTH)
|
||||
{
|
||||
remote_url = remote_url
|
||||
.chars()
|
||||
.skip(
|
||||
remote_url.len()
|
||||
- url_length.saturating_sub(
|
||||
THREE_DOTS_LENGTH,
|
||||
),
|
||||
)
|
||||
.collect::<String>();
|
||||
remote_url = format!("{THREE_DOTS}{remote_url}");
|
||||
}
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(
|
||||
"{REMOTE_URL_LABEL}{remote_url:url_length$}"
|
||||
),
|
||||
self.theme.text(true, false),
|
||||
)));
|
||||
}
|
||||
f.render_widget(
|
||||
Paragraph::new(Text::from(lines))
|
||||
.alignment(Alignment::Left)
|
||||
.wrap(Wrap { trim: true }),
|
||||
r,
|
||||
);
|
||||
|
||||
let mut r = r;
|
||||
r.width += 1;
|
||||
r.height += 2;
|
||||
r.y = r.y.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
fn move_selection(&mut self, scroll: ScrollType) -> Result<bool> {
|
||||
let new_selection = match scroll {
|
||||
ScrollType::Up => self.selection.saturating_add(1),
|
||||
ScrollType::Down => self.selection.saturating_sub(1),
|
||||
ScrollType::PageDown => self
|
||||
.selection
|
||||
.saturating_add(self.current_height.get()),
|
||||
ScrollType::PageUp => self
|
||||
.selection
|
||||
.saturating_sub(self.current_height.get()),
|
||||
ScrollType::Home => 0,
|
||||
ScrollType::End => {
|
||||
let num_branches: u16 =
|
||||
self.remote_names.len().try_into()?;
|
||||
num_branches.saturating_sub(1)
|
||||
}
|
||||
};
|
||||
|
||||
self.set_selection(new_selection)?;
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn valid_selection(&self) -> bool {
|
||||
!self.remote_names.is_empty()
|
||||
&& self.remote_names.len() >= self.selection as usize
|
||||
}
|
||||
|
||||
fn set_selection(&mut self, selection: u16) -> Result<()> {
|
||||
let num_remotes: u16 = self.remote_names.len().try_into()?;
|
||||
let num_remotes = num_remotes.saturating_sub(1);
|
||||
|
||||
let selection = if selection > num_remotes {
|
||||
num_remotes
|
||||
} else {
|
||||
selection
|
||||
};
|
||||
|
||||
self.selection = selection;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update_remotes(&mut self) -> Result<()> {
|
||||
if self.is_visible() {
|
||||
self.remote_names = get_remotes(&self.repo.borrow())?;
|
||||
self.set_selection(self.selection)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn delete_remote(&self) {
|
||||
let remote_name =
|
||||
self.remote_names[self.selection as usize].clone();
|
||||
|
||||
self.queue.push(InternalEvent::ConfirmAction(
|
||||
Action::DeleteRemote(remote_name),
|
||||
));
|
||||
}
|
||||
|
||||
fn rename_remote(&self) {
|
||||
let remote_name =
|
||||
self.remote_names[self.selection as usize].clone();
|
||||
|
||||
self.queue.push(InternalEvent::RenameRemote(remote_name));
|
||||
}
|
||||
|
||||
fn update_remote_url(&self) {
|
||||
let remote_name =
|
||||
self.remote_names[self.selection as usize].clone();
|
||||
let remote_url =
|
||||
get_remote_url(&self.repo.borrow(), &remote_name);
|
||||
if let Ok(Some(url)) = remote_url {
|
||||
self.queue.push(InternalEvent::UpdateRemoteUrl(
|
||||
remote_name,
|
||||
url,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
176
src/popups/rename_remote.rs
Normal file
176
src/popups/rename_remote.rs
Normal file
@ -0,0 +1,176 @@
|
||||
use anyhow::Result;
|
||||
use asyncgit::sync::{self, RepoPathRef};
|
||||
use crossterm::event::Event;
|
||||
use easy_cast::Cast;
|
||||
use ratatui::{layout::Rect, widgets::Paragraph, Frame};
|
||||
|
||||
use crate::{
|
||||
app::Environment,
|
||||
components::{
|
||||
visibility_blocking, CommandBlocking, CommandInfo, Component,
|
||||
DrawableComponent, EventState, InputType, TextInputComponent,
|
||||
},
|
||||
keys::{key_match, SharedKeyConfig},
|
||||
queue::{InternalEvent, NeedsUpdate, Queue},
|
||||
strings,
|
||||
ui::style::SharedTheme,
|
||||
};
|
||||
|
||||
pub struct RenameRemotePopup {
|
||||
repo: RepoPathRef,
|
||||
input: TextInputComponent,
|
||||
theme: SharedTheme,
|
||||
key_config: SharedKeyConfig,
|
||||
queue: Queue,
|
||||
initial_name: Option<String>,
|
||||
}
|
||||
|
||||
impl DrawableComponent for RenameRemotePopup {
|
||||
fn draw(&self, f: &mut Frame, rect: Rect) -> Result<()> {
|
||||
if self.is_visible() {
|
||||
self.input.draw(f, rect)?;
|
||||
self.draw_warnings(f);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for RenameRemotePopup {
|
||||
fn commands(
|
||||
&self,
|
||||
out: &mut Vec<CommandInfo>,
|
||||
force_all: bool,
|
||||
) -> CommandBlocking {
|
||||
if self.is_visible() || force_all {
|
||||
self.input.commands(out, force_all);
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::remote_confirm_name_msg(
|
||||
&self.key_config,
|
||||
),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
}
|
||||
visibility_blocking(self)
|
||||
}
|
||||
|
||||
fn event(&mut self, ev: &Event) -> Result<EventState> {
|
||||
if self.is_visible() {
|
||||
if self.input.event(ev)?.is_consumed() {
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
|
||||
if let Event::Key(e) = ev {
|
||||
if key_match(e, self.key_config.keys.enter) {
|
||||
self.rename_remote();
|
||||
}
|
||||
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
}
|
||||
Ok(EventState::NotConsumed)
|
||||
}
|
||||
|
||||
fn is_visible(&self) -> bool {
|
||||
self.input.is_visible()
|
||||
}
|
||||
|
||||
fn hide(&mut self) {
|
||||
self.input.hide();
|
||||
}
|
||||
|
||||
fn show(&mut self) -> Result<()> {
|
||||
self.input.show()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl RenameRemotePopup {
|
||||
///
|
||||
pub fn new(env: &Environment) -> Self {
|
||||
Self {
|
||||
repo: env.repo.clone(),
|
||||
input: TextInputComponent::new(
|
||||
env,
|
||||
&strings::rename_remote_popup_title(&env.key_config),
|
||||
&strings::rename_remote_popup_msg(&env.key_config),
|
||||
true,
|
||||
)
|
||||
.with_input_type(InputType::Singleline),
|
||||
theme: env.theme.clone(),
|
||||
key_config: env.key_config.clone(),
|
||||
queue: env.queue.clone(),
|
||||
initial_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn open(&mut self, cur_name: String) -> Result<()> {
|
||||
self.input.set_text(cur_name.clone());
|
||||
self.initial_name = Some(cur_name);
|
||||
self.show()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_warnings(&self, f: &mut Frame) {
|
||||
let current_text = self.input.get_text();
|
||||
|
||||
if !current_text.is_empty() {
|
||||
let valid = sync::validate_remote_name(current_text);
|
||||
|
||||
if !valid {
|
||||
let msg = strings::branch_name_invalid();
|
||||
let msg_length: u16 = msg.len().cast();
|
||||
let w = Paragraph::new(msg)
|
||||
.style(self.theme.text_danger());
|
||||
|
||||
let rect = {
|
||||
let mut rect = self.input.get_area();
|
||||
rect.y += rect.height.saturating_sub(1);
|
||||
rect.height = 1;
|
||||
let offset =
|
||||
rect.width.saturating_sub(msg_length + 1);
|
||||
rect.width =
|
||||
rect.width.saturating_sub(offset + 1);
|
||||
rect.x += offset;
|
||||
|
||||
rect
|
||||
};
|
||||
|
||||
f.render_widget(w, rect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn rename_remote(&mut self) {
|
||||
if let Some(init_name) = &self.initial_name {
|
||||
if init_name != self.input.get_text() {
|
||||
let res = sync::rename_remote(
|
||||
&self.repo.borrow(),
|
||||
init_name,
|
||||
self.input.get_text(),
|
||||
);
|
||||
match res {
|
||||
Ok(()) => {
|
||||
self.queue.push(InternalEvent::Update(
|
||||
NeedsUpdate::ALL | NeedsUpdate::REMOTES,
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("rename remote: {}", e,);
|
||||
self.queue.push(InternalEvent::ShowErrorMsg(
|
||||
format!("rename remote error:\n{e}",),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.input.clear();
|
||||
self.initial_name = None;
|
||||
self.hide();
|
||||
}
|
||||
}
|
152
src/popups/update_remote_url.rs
Normal file
152
src/popups/update_remote_url.rs
Normal file
@ -0,0 +1,152 @@
|
||||
use anyhow::Result;
|
||||
use asyncgit::sync::{self, RepoPathRef};
|
||||
use crossterm::event::Event;
|
||||
|
||||
use crate::{
|
||||
app::Environment,
|
||||
components::{
|
||||
visibility_blocking, CommandBlocking, CommandInfo, Component,
|
||||
DrawableComponent, EventState, InputType, TextInputComponent,
|
||||
},
|
||||
keys::{key_match, SharedKeyConfig},
|
||||
queue::{InternalEvent, NeedsUpdate, Queue},
|
||||
strings,
|
||||
};
|
||||
|
||||
pub struct UpdateRemoteUrlPopup {
|
||||
repo: RepoPathRef,
|
||||
input: TextInputComponent,
|
||||
key_config: SharedKeyConfig,
|
||||
queue: Queue,
|
||||
remote_name: Option<String>,
|
||||
initial_url: Option<String>,
|
||||
}
|
||||
|
||||
impl DrawableComponent for UpdateRemoteUrlPopup {
|
||||
fn draw(
|
||||
&self,
|
||||
f: &mut ratatui::Frame,
|
||||
rect: ratatui::prelude::Rect,
|
||||
) -> anyhow::Result<()> {
|
||||
if self.is_visible() {
|
||||
self.input.draw(f, rect)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for UpdateRemoteUrlPopup {
|
||||
fn commands(
|
||||
&self,
|
||||
out: &mut Vec<crate::components::CommandInfo>,
|
||||
force_all: bool,
|
||||
) -> CommandBlocking {
|
||||
if self.is_visible() || force_all {
|
||||
self.input.commands(out, force_all);
|
||||
|
||||
out.push(CommandInfo::new(
|
||||
strings::commands::remote_confirm_url_msg(
|
||||
&self.key_config,
|
||||
),
|
||||
true,
|
||||
true,
|
||||
));
|
||||
}
|
||||
visibility_blocking(self)
|
||||
}
|
||||
|
||||
fn event(&mut self, ev: &Event) -> Result<EventState> {
|
||||
if self.is_visible() {
|
||||
if self.input.event(ev)?.is_consumed() {
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
|
||||
if let Event::Key(e) = ev {
|
||||
if key_match(e, self.key_config.keys.enter) {
|
||||
self.update_remote_url();
|
||||
}
|
||||
|
||||
return Ok(EventState::Consumed);
|
||||
}
|
||||
}
|
||||
Ok(EventState::NotConsumed)
|
||||
}
|
||||
|
||||
fn is_visible(&self) -> bool {
|
||||
self.input.is_visible()
|
||||
}
|
||||
|
||||
fn hide(&mut self) {
|
||||
self.input.hide();
|
||||
}
|
||||
|
||||
fn show(&mut self) -> Result<()> {
|
||||
self.input.show()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateRemoteUrlPopup {
|
||||
pub fn new(env: &Environment) -> Self {
|
||||
Self {
|
||||
repo: env.repo.clone(),
|
||||
input: TextInputComponent::new(
|
||||
env,
|
||||
&strings::update_remote_url_popup_title(
|
||||
&env.key_config,
|
||||
),
|
||||
&strings::update_remote_url_popup_msg(
|
||||
&env.key_config,
|
||||
),
|
||||
true,
|
||||
)
|
||||
.with_input_type(InputType::Singleline),
|
||||
key_config: env.key_config.clone(),
|
||||
queue: env.queue.clone(),
|
||||
initial_url: None,
|
||||
remote_name: None,
|
||||
}
|
||||
}
|
||||
|
||||
///
|
||||
pub fn open(
|
||||
&mut self,
|
||||
remote_name: String,
|
||||
cur_url: String,
|
||||
) -> Result<()> {
|
||||
self.input.set_text(cur_url.clone());
|
||||
self.remote_name = Some(remote_name);
|
||||
self.initial_url = Some(cur_url);
|
||||
self.show()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
///
|
||||
pub fn update_remote_url(&mut self) {
|
||||
if let Some(remote_name) = &self.remote_name {
|
||||
let res = sync::update_remote_url(
|
||||
&self.repo.borrow(),
|
||||
remote_name,
|
||||
self.input.get_text(),
|
||||
);
|
||||
match res {
|
||||
Ok(()) => {
|
||||
self.queue.push(InternalEvent::Update(
|
||||
NeedsUpdate::ALL | NeedsUpdate::REMOTES,
|
||||
));
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("update remote url: {}", e,);
|
||||
self.queue.push(InternalEvent::ShowErrorMsg(
|
||||
format!("update remote url error:\n{e}",),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.input.clear();
|
||||
self.initial_url = None;
|
||||
self.hide();
|
||||
}
|
||||
}
|
11
src/queue.rs
11
src/queue.rs
@ -28,6 +28,8 @@ bitflags! {
|
||||
const COMMANDS = 0b100;
|
||||
/// branches have changed
|
||||
const BRANCHES = 0b1000;
|
||||
/// Remotes have changed
|
||||
const REMOTES = 0b1001;
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,6 +50,7 @@ pub enum Action {
|
||||
DeleteRemoteBranch(String),
|
||||
DeleteTag(String),
|
||||
DeleteRemoteTag(String, String),
|
||||
DeleteRemote(String),
|
||||
ForcePush(String, bool),
|
||||
PullMerge { incoming: usize, rebase: bool },
|
||||
AbortMerge,
|
||||
@ -109,6 +112,10 @@ pub enum InternalEvent {
|
||||
///
|
||||
CreateBranch,
|
||||
///
|
||||
RenameRemote(String),
|
||||
///
|
||||
UpdateRemoteUrl(String, String),
|
||||
///
|
||||
RenameBranch(String, String),
|
||||
///
|
||||
SelectBranch,
|
||||
@ -139,6 +146,10 @@ pub enum InternalEvent {
|
||||
///
|
||||
ViewSubmodules,
|
||||
///
|
||||
ViewRemotes,
|
||||
///
|
||||
CreateRemote,
|
||||
///
|
||||
OpenRepo { path: PathBuf },
|
||||
///
|
||||
OpenResetPopup(CommitId),
|
||||
|
149
src/strings.rs
149
src/strings.rs
@ -30,6 +30,8 @@ pub static PUSH_TAGS_STATES_PUSHING: &str = "pushing";
|
||||
pub static PUSH_TAGS_STATES_DONE: &str = "done";
|
||||
|
||||
pub static POPUP_TITLE_SUBMODULES: &str = "Submodules";
|
||||
pub static POPUP_TITLE_REMOTES: &str = "Remotes";
|
||||
pub static POPUP_SUBTITLE_REMOTES: &str = "Details";
|
||||
pub static POPUP_TITLE_FUZZY_FIND: &str = "Fuzzy Finder";
|
||||
pub static POPUP_TITLE_LOG_SEARCH: &str = "Search";
|
||||
|
||||
@ -251,6 +253,17 @@ pub fn confirm_title_delete_remote_branch(
|
||||
) -> String {
|
||||
"Delete Remote Branch".to_string()
|
||||
}
|
||||
pub fn confirm_title_delete_remote(
|
||||
_key_config: &SharedKeyConfig,
|
||||
) -> String {
|
||||
"Delete Remote".to_string()
|
||||
}
|
||||
pub fn confirm_msg_delete_remote(
|
||||
_key_config: &SharedKeyConfig,
|
||||
remote_name: &str,
|
||||
) -> String {
|
||||
format!("Confirm deleting remote \"{remote_name}\"")
|
||||
}
|
||||
pub fn confirm_msg_delete_remote_branch(
|
||||
_key_config: &SharedKeyConfig,
|
||||
branch_ref: &str,
|
||||
@ -339,6 +352,49 @@ pub fn create_branch_popup_msg(
|
||||
) -> String {
|
||||
"type branch name".to_string()
|
||||
}
|
||||
pub fn rename_remote_popup_title(
|
||||
_key_config: &SharedKeyConfig,
|
||||
) -> String {
|
||||
"Rename remote".to_string()
|
||||
}
|
||||
pub fn rename_remote_popup_msg(
|
||||
_key_config: &SharedKeyConfig,
|
||||
) -> String {
|
||||
"new remote name".to_string()
|
||||
}
|
||||
pub fn update_remote_url_popup_title(
|
||||
_key_config: &SharedKeyConfig,
|
||||
) -> String {
|
||||
"Update url".to_string()
|
||||
}
|
||||
pub fn update_remote_url_popup_msg(
|
||||
_key_config: &SharedKeyConfig,
|
||||
) -> String {
|
||||
"new remote url".to_string()
|
||||
}
|
||||
pub fn create_remote_popup_title_name(
|
||||
_key_config: &SharedKeyConfig,
|
||||
) -> String {
|
||||
"Remote name".to_string()
|
||||
}
|
||||
pub fn create_remote_popup_title_url(
|
||||
_key_config: &SharedKeyConfig,
|
||||
) -> String {
|
||||
"Remote url".to_string()
|
||||
}
|
||||
pub fn create_remote_popup_msg_name(
|
||||
_key_config: &SharedKeyConfig,
|
||||
) -> String {
|
||||
"type remote name".to_string()
|
||||
}
|
||||
pub fn create_remote_popup_msg_url(
|
||||
_key_config: &SharedKeyConfig,
|
||||
) -> String {
|
||||
"type remote url".to_string()
|
||||
}
|
||||
pub const fn remote_name_invalid() -> &'static str {
|
||||
"[invalid name]"
|
||||
}
|
||||
pub fn username_popup_title(_key_config: &SharedKeyConfig) -> String {
|
||||
"Username".to_string()
|
||||
}
|
||||
@ -830,6 +886,99 @@ pub mod commands {
|
||||
)
|
||||
}
|
||||
|
||||
pub fn view_remotes(key_config: &SharedKeyConfig) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Remotes [{}]",
|
||||
key_config.get_hint(key_config.keys.view_remotes)
|
||||
),
|
||||
"open remotes view",
|
||||
CMD_GROUP_GENERAL,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update_remote_name(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Edit name [{}]",
|
||||
key_config
|
||||
.get_hint(key_config.keys.update_remote_name)
|
||||
),
|
||||
"updates a remote name",
|
||||
CMD_GROUP_GENERAL,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn update_remote_url(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Edit url [{}]",
|
||||
key_config
|
||||
.get_hint(key_config.keys.update_remote_url)
|
||||
),
|
||||
"updates a remote url",
|
||||
CMD_GROUP_GENERAL,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn create_remote(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Add [{}]",
|
||||
key_config.get_hint(key_config.keys.add_remote)
|
||||
),
|
||||
"creates a new remote",
|
||||
CMD_GROUP_GENERAL,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn delete_remote_popup(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Remove [{}]",
|
||||
key_config.get_hint(key_config.keys.delete_remote),
|
||||
),
|
||||
"remove a remote",
|
||||
CMD_GROUP_BRANCHES,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn remote_confirm_name_msg(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Confirm name [{}]",
|
||||
key_config.get_hint(key_config.keys.enter),
|
||||
),
|
||||
"confirm remote name",
|
||||
CMD_GROUP_BRANCHES,
|
||||
)
|
||||
.hide_help()
|
||||
}
|
||||
|
||||
pub fn remote_confirm_url_msg(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
CommandText::new(
|
||||
format!(
|
||||
"Confirm url [{}]",
|
||||
key_config.get_hint(key_config.keys.enter),
|
||||
),
|
||||
"confirm remote url",
|
||||
CMD_GROUP_BRANCHES,
|
||||
)
|
||||
.hide_help()
|
||||
}
|
||||
|
||||
pub fn open_submodule(
|
||||
key_config: &SharedKeyConfig,
|
||||
) -> CommandText {
|
||||
|
Loading…
Reference in New Issue
Block a user