diff --git a/src/main/composeResources/values/strings.xml b/src/main/composeResources/values/strings.xml index 1fd3bd64..80b78833 100644 --- a/src/main/composeResources/values/strings.xml +++ b/src/main/composeResources/values/strings.xml @@ -37,6 +37,8 @@ Pop the last stash Terminal Open a terminal in the repository's path + Editor + Open a repository in external editor Actions Additional actions Settings diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/editor/OpenRepositoryInEditorUseCase.kt b/src/main/kotlin/com/jetpackduba/gitnuro/editor/OpenRepositoryInEditorUseCase.kt new file mode 100644 index 00000000..a0443ef3 --- /dev/null +++ b/src/main/kotlin/com/jetpackduba/gitnuro/editor/OpenRepositoryInEditorUseCase.kt @@ -0,0 +1,34 @@ +package com.jetpackduba.gitnuro.editor + +import com.jetpackduba.gitnuro.managers.ErrorsManager +import com.jetpackduba.gitnuro.managers.IShellManager +import com.jetpackduba.gitnuro.models.errorNotification +import com.jetpackduba.gitnuro.repositories.AppSettingsRepository +import javax.inject.Inject + +/** + * Use-case for opening the repository directory with an external editor. + */ +class OpenRepositoryInEditorUseCase @Inject constructor( + private val settings: AppSettingsRepository, + private val shellManager: IShellManager, + private val errorsManager: ErrorsManager +) { + /** + * Opens the given path in the configured external editor. + * @param path The path to open. Should be a directory. + */ + suspend operator fun invoke(path: String) { + val editor = settings.editor; + val isSet = editor.isNotEmpty(); + + if (!isSet) { + errorsManager.emitNotification( + errorNotification("No editor configured") + ) + return + } + + shellManager.runCommandInPath(listOf(editor, path), path) + } +} diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/keybindings/Keybinding.kt b/src/main/kotlin/com/jetpackduba/gitnuro/keybindings/Keybinding.kt index ad9feba0..3d158ca6 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/keybindings/Keybinding.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/keybindings/Keybinding.kt @@ -97,6 +97,11 @@ enum class KeybindingOption { * Used to open the settings screen */ SETTINGS, + + /** + * Used to open the current repository in an external editor + */ + OPEN_EDITOR } @@ -156,6 +161,9 @@ private fun baseKeybindings() = mapOf( KeybindingOption.SETTINGS to listOf( Keybinding(key = Key.S, control = true, alt = true), ), + KeybindingOption.OPEN_EDITOR to listOf( + Keybinding(key = Key.A, control = true, shift = true), + ), ) private fun linuxKeybindings(): Map> = baseKeybindings() @@ -179,6 +187,7 @@ private fun macKeybindings(): Map> { KeybindingOption.CHANGE_CURRENT_TAB_RIGHT, KeybindingOption.CHANGE_CURRENT_TAB_LEFT, KeybindingOption.SETTINGS, + KeybindingOption.OPEN_EDITOR, ) for (key in keysToReplaceControlWithCommand) { diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/repositories/AppSettingsRepository.kt b/src/main/kotlin/com/jetpackduba/gitnuro/repositories/AppSettingsRepository.kt index 3820ae2d..17155a44 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/repositories/AppSettingsRepository.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/repositories/AppSettingsRepository.kt @@ -61,6 +61,8 @@ private const val PREF_GIT_PUSH_WITH_LEASE = "gitPushWithLease" private const val PREF_VERIFY_SSL = "verifySsl" +private const val PREF_EDITOR = "editor" + private const val DEFAULT_SWAP_UNCOMMITTED_CHANGES = false private const val DEFAULT_SHOW_CHANGES_AS_TREE = false private const val DEFAULT_CACHE_CREDENTIALS_IN_MEMORY = true @@ -91,6 +93,9 @@ class AppSettingsRepository @Inject constructor() { private val _verifySslFlow = MutableStateFlow(cacheCredentialsInMemory) val verifySslFlow = _verifySslFlow.asStateFlow() + private val _editorFlow = MutableStateFlow(editor) + val editorFlow = _editorFlow.asStateFlow() + private val _defaultCloneDirFlow = MutableStateFlow(defaultCloneDir) val defaultCloneDirFlow = _defaultCloneDirFlow.asStateFlow() @@ -241,6 +246,15 @@ class AppSettingsRepository @Inject constructor() { _verifySslFlow.value = value } + var editor: String + get() { + return preferences.get(PREF_EDITOR, "") + } + set(value) { + preferences.put(PREF_EDITOR, value) + _editorFlow.value = value + } + var scaleUi: Float get() { return preferences.getFloat(PREF_UI_SCALE, DEFAULT_UI_SCALE) diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Menu.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Menu.kt index 2a7d9996..f4b237c2 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/Menu.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/Menu.kt @@ -174,6 +174,15 @@ fun Menu( keybinding = null, ) + MenuButton( + modifier = Modifier.padding(end = 4.dp), + title = stringResource(Res.string.menu_editor), + icon = painterResource(Res.drawable.edit), + onClick = { menuViewModel.openEditor() }, + tooltip = stringResource(Res.string.menu_editor_tooltip), + keybinding = KeybindingOption.OPEN_EDITOR.keyBinding, + ) + MenuButton( modifier = Modifier.padding(end = 4.dp), title = stringResource(Res.string.menu_actions), diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt index 1d7bf5b9..174dbc8b 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/RepositoryOpen.kt @@ -94,6 +94,7 @@ fun RepositoryOpenPage( showQuickActionsDialog = false when (it) { QuickActionType.OPEN_DIR_IN_FILE_MANAGER -> repositoryOpenViewModel.openFolderInFileExplorer() + QuickActionType.OPEN_DIR_IN_EDITOR -> repositoryOpenViewModel.openFolderInEditor() QuickActionType.CLONE -> onShowCloneDialog() QuickActionType.REFRESH -> repositoryOpenViewModel.refreshAll() QuickActionType.SIGN_OFF -> showSignOffDialog = true @@ -165,6 +166,11 @@ fun RepositoryOpenPage( true } + it.matchesBinding(KeybindingOption.OPEN_EDITOR) -> { + repositoryOpenViewModel.openFolderInEditor() + true + } + else -> false } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/QuickActionsDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/QuickActionsDialog.kt index 62f45e7f..d1c03525 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/QuickActionsDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/QuickActionsDialog.kt @@ -36,6 +36,7 @@ fun QuickActionsDialog( val items = remember { listOf( QuickAction(Res.drawable.code, "Open repository in file manager", QuickActionType.OPEN_DIR_IN_FILE_MANAGER), + QuickAction(Res.drawable.code, "Open repository in editor", QuickActionType.OPEN_DIR_IN_EDITOR), QuickAction(Res.drawable.download, "Clone new repository", QuickActionType.CLONE), QuickAction(Res.drawable.refresh, "Refresh repository data", QuickActionType.REFRESH), QuickAction(Res.drawable.sign, "Signoff config", QuickActionType.SIGN_OFF), @@ -128,6 +129,7 @@ data class QuickAction(val icon: DrawableResource, val title: String, val type: enum class QuickActionType { OPEN_DIR_IN_FILE_MANAGER, + OPEN_DIR_IN_EDITOR, CLONE, REFRESH, SIGN_OFF diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt index 6ee0b9a1..aabe95c1 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/ui/dialogs/settings/SettingsDialog.kt @@ -73,6 +73,7 @@ val settings = listOf( SettingsEntry.Section("Tools"), SettingsEntry.Entry(Res.drawable.terminal, "Terminal") { Terminal(it) }, + SettingsEntry.Entry(Res.drawable.edit, "Editor") { Editor(it) }, SettingsEntry.Entry(Res.drawable.info, "Logs") { Logs(it) }, ) @@ -357,6 +358,20 @@ private fun Security(settingsViewModel: SettingsViewModel) { ) } +@Composable +fun Editor(settingsViewModel: SettingsViewModel) { + val editor by settingsViewModel.editorFlow.collectAsState() + + SettingTextInput( + title = "External editor path", + subtitle = "Configure an external editor to open the repository with", + value = editor, + onValueChanged = { value -> + settingsViewModel.editor = value + }, + ) +} + @Composable fun Terminal(settingsViewModel: SettingsViewModel) { var commitsLimit by remember { mutableStateOf(settingsViewModel.terminalPath) } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/GlobalMenuActionsViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/GlobalMenuActionsViewModel.kt index 2f99cce5..49f64946 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/GlobalMenuActionsViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/GlobalMenuActionsViewModel.kt @@ -1,6 +1,7 @@ package com.jetpackduba.gitnuro.viewmodels import com.jetpackduba.gitnuro.TaskType +import com.jetpackduba.gitnuro.editor.OpenRepositoryInEditorUseCase import com.jetpackduba.gitnuro.git.RefreshType import com.jetpackduba.gitnuro.git.TabState import com.jetpackduba.gitnuro.git.remote_operations.FetchAllRemotesUseCase @@ -23,6 +24,7 @@ interface IGlobalMenuActionsViewModel { fun stash(): Job fun popStash(): Job fun openTerminal(): Job + fun openEditor(): Job } class GlobalMenuActionsViewModel @Inject constructor( @@ -33,6 +35,7 @@ class GlobalMenuActionsViewModel @Inject constructor( private val popLastStashUseCase: PopLastStashUseCase, private val stashChangesUseCase: StashChangesUseCase, private val openRepositoryInTerminalUseCase: OpenRepositoryInTerminalUseCase, + private val openRepositoryInEditorUseCase: OpenRepositoryInEditorUseCase, ) : IGlobalMenuActionsViewModel { override fun pull(pullType: PullType) = tabState.safeProcessing( refreshType = RefreshType.ALL_DATA, @@ -100,4 +103,10 @@ class GlobalMenuActionsViewModel @Inject constructor( ) { git -> openRepositoryInTerminalUseCase(git.repository.workTree.absolutePath) } + + override fun openEditor() = tabState.runOperation( + refreshType = RefreshType.NONE + ) { git -> + openRepositoryInEditorUseCase(git.repository.workTree.absolutePath) + } } \ No newline at end of file diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RepositoryOpenViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RepositoryOpenViewModel.kt index 5733a468..e6c42057 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RepositoryOpenViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/RepositoryOpenViewModel.kt @@ -2,6 +2,7 @@ package com.jetpackduba.gitnuro.viewmodels import com.jetpackduba.gitnuro.SharedRepositoryStateManager import com.jetpackduba.gitnuro.TaskType +import com.jetpackduba.gitnuro.editor.OpenRepositoryInEditorUseCase import com.jetpackduba.gitnuro.exceptions.codeToMessage import com.jetpackduba.gitnuro.git.* import com.jetpackduba.gitnuro.git.branches.CreateBranchUseCase @@ -12,10 +13,12 @@ import com.jetpackduba.gitnuro.logging.printDebug import com.jetpackduba.gitnuro.logging.printLog import com.jetpackduba.gitnuro.managers.AppStateManager import com.jetpackduba.gitnuro.managers.ErrorsManager +import com.jetpackduba.gitnuro.managers.IShellManager import com.jetpackduba.gitnuro.managers.newErrorNow import com.jetpackduba.gitnuro.models.AuthorInfoSimple import com.jetpackduba.gitnuro.models.errorNotification import com.jetpackduba.gitnuro.models.positiveNotification +import com.jetpackduba.gitnuro.repositories.AppSettingsRepository import com.jetpackduba.gitnuro.system.OpenFilePickerUseCase import com.jetpackduba.gitnuro.system.OpenUrlInBrowserUseCase import com.jetpackduba.gitnuro.system.PickerType @@ -62,6 +65,7 @@ class RepositoryOpenViewModel @Inject constructor( private val stashChangesUseCase: StashChangesUseCase, private val stageUntrackedFileUseCase: StageUntrackedFileUseCase, private val openFilePickerUseCase: OpenFilePickerUseCase, + private val openInEditorUseCase: OpenRepositoryInEditorUseCase, private val openUrlInBrowserUseCase: OpenUrlInBrowserUseCase, private val tabsManager: TabsManager, private val tabScope: CoroutineScope, @@ -352,6 +356,13 @@ class RepositoryOpenViewModel @Inject constructor( Desktop.getDesktop().open(git.repository.workTree) } + fun openFolderInEditor() = tabState.runOperation( + showError = true, + refreshType = RefreshType.NONE, + ) { git -> + openInEditorUseCase(git.repository.workTree.path) + } + fun openUrlInBrowser(url: String) { openUrlInBrowserUseCase(url) } diff --git a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt index cf0da061..e6dc0042 100644 --- a/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt +++ b/src/main/kotlin/com/jetpackduba/gitnuro/viewmodels/SettingsViewModel.kt @@ -43,6 +43,7 @@ class SettingsViewModel @Inject constructor( val swapUncommittedChangesFlow = appSettingsRepository.swapUncommittedChangesFlow val cacheCredentialsInMemoryFlow = appSettingsRepository.cacheCredentialsInMemoryFlow val verifySslFlow = appSettingsRepository.verifySslFlow + val editorFlow = appSettingsRepository.editorFlow val terminalPathFlow = appSettingsRepository.terminalPathFlow val avatarProviderFlow = appSettingsRepository.avatarProviderTypeFlow val dateFormatFlow = appSettingsRepository.dateTimeFormatFlow @@ -90,6 +91,12 @@ class SettingsViewModel @Inject constructor( appSettingsRepository.verifySsl = value } + var editor: String + get() = appSettingsRepository.editor + set(value) { + appSettingsRepository.editor = value + } + var pullRebase: Boolean get() = appSettingsRepository.pullRebase set(value) {