Skip to content

#303 Open File in Folder #304

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package com.jetpackduba.gitnuro.extensions

import com.jetpackduba.gitnuro.logging.printError
import com.jetpackduba.gitnuro.system.OS
import com.jetpackduba.gitnuro.system.currentOs
import kotlinx.io.IOException
import java.awt.Desktop
import java.io.File

private const val TAG = "FileExtensions"

fun File.openDirectory(dirName: String): File {
val newDir = File(this, dirName)

Expand All @@ -10,4 +17,43 @@ fun File.openDirectory(dirName: String): File {
}

return newDir
}

fun File.openFileInFolder() {

if (!exists() || !isDirectory) {
printError(TAG, "Folder with path $path does not exist or is not a folder")
return
}

try {
if (Desktop.isDesktopSupported()) {
val desktop = Desktop.getDesktop()
if (desktop.isSupported(Desktop.Action.OPEN)) {
desktop.open(this)
return
}
}
} catch (e: Exception) {
printError(TAG, "Desktop API failed: ${e.message}")
}

// Fallback
val os = currentOs
val command = when (os) {
OS.LINUX -> listOf("xdg-open", absolutePath)
OS.MAC -> listOf("open", absolutePath)
OS.WINDOWS -> listOf("explorer", absolutePath)
else -> null
}

if (command != null) {
try {
ProcessBuilder(command).start()
} catch (ex: IOException) {
printError(TAG, "Failed to open file explorer: ${ex.message}")
}
} else {
printError(TAG, "Unsupported OS: $os")
}
}
4 changes: 4 additions & 0 deletions src/main/kotlin/com/jetpackduba/gitnuro/ui/CommitChanges.kt
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ fun CommitChanges(
commitChangesStatus = commitChangesStatus,
onBlame = onBlame,
onHistory = onHistory,
onOpenFileInFolder = { commitChangesViewModel.openFileInFolder(it) },
showSearch = showSearch,
showAsTree = showAsTree,
changesListScroll = changesListScroll,
Expand Down Expand Up @@ -121,6 +122,7 @@ private fun CommitChangesView(
searchFilter: TextFieldValue,
onBlame: (String) -> Unit,
onHistory: (String) -> Unit,
onOpenFileInFolder: (String) -> Unit,
onDiffSelected: (DiffEntry) -> Unit,
onSearchFilterToggled: (Boolean) -> Unit,
onSearchFocused: () -> Unit,
Expand Down Expand Up @@ -167,6 +169,7 @@ private fun CommitChangesView(
diffEntry,
onBlame = { onBlame(diffEntry.filePath) },
onHistory = { onHistory(diffEntry.filePath) },
onOpenFileInFolder = { onOpenFileInFolder(diffEntry.parentDirectoryPath) },
)
}
)
Expand All @@ -183,6 +186,7 @@ private fun CommitChangesView(
diffEntry,
onBlame = { onBlame(diffEntry.filePath) },
onHistory = { onHistory(diffEntry.filePath) },
onOpenFileInFolder = { onOpenFileInFolder(diffEntry.parentDirectoryPath) },
)
},
onDirectoryClicked = onDirectoryClicked,
Expand Down
12 changes: 9 additions & 3 deletions src/main/kotlin/com/jetpackduba/gitnuro/ui/UncommitedChanges.kt
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ fun UncommittedChanges(
onHistoryFile = onHistoryFile,
onReset = { statusViewModel.resetStaged(it) },
onDelete = { statusViewModel.deleteFile(it) },
onOpenFileInFolder = { statusViewModel.openFileInFolder(it) },
onAllAction = { statusViewModel.unstageAll() },
onAlternateShowAsTree = { statusViewModel.alternateShowAsTree() },
onTreeDirectoryClicked = { statusViewModel.stagedTreeDirectoryClicked(it) },
Expand All @@ -189,6 +190,7 @@ fun UncommittedChanges(
onHistoryFile = onHistoryFile,
onReset = { statusViewModel.resetUnstaged(it) },
onDelete = { statusViewModel.deleteFile(it) },
onOpenFileInFolder = { statusViewModel.openFileInFolder(it) },
onAllAction = { statusViewModel.stageAll() },
onAlternateShowAsTree = { statusViewModel.alternateShowAsTree() },
onTreeDirectoryClicked = { statusViewModel.stagedTreeDirectoryClicked(it) },
Expand Down Expand Up @@ -359,6 +361,7 @@ fun ColumnScope.StagedView(
onHistoryFile: (String) -> Unit,
onReset: (StatusEntry) -> Unit,
onDelete: (StatusEntry) -> Unit,
onOpenFileInFolder: (String?) -> Unit,
onAllAction: () -> Unit,
onAlternateShowAsTree: () -> Unit,
onTreeDirectoryClicked: (String) -> Unit,
Expand Down Expand Up @@ -393,6 +396,7 @@ fun ColumnScope.StagedView(
onHistoryFile = onHistoryFile,
onReset = onReset,
onDelete = onDelete,
onOpenFileInFolder = onOpenFileInFolder,
onAllAction = onAllAction,
onAlternateShowAsTree = onAlternateShowAsTree,
onTreeDirectoryClicked = onTreeDirectoryClicked,
Expand Down Expand Up @@ -431,6 +435,7 @@ fun ColumnScope.UnstagedView(
onHistoryFile: (String) -> Unit,
onReset: (StatusEntry) -> Unit,
onDelete: (StatusEntry) -> Unit,
onOpenFileInFolder: (String?) -> Unit,
onAllAction: () -> Unit,
onAlternateShowAsTree: () -> Unit,
onTreeDirectoryClicked: (String) -> Unit,
Expand Down Expand Up @@ -465,6 +470,7 @@ fun ColumnScope.UnstagedView(
onHistoryFile = onHistoryFile,
onReset = onReset,
onDelete = onDelete,
onOpenFileInFolder = onOpenFileInFolder,
onAllAction = onAllAction,
onAlternateShowAsTree = onAlternateShowAsTree,
onTreeDirectoryClicked = onTreeDirectoryClicked,
Expand Down Expand Up @@ -512,6 +518,7 @@ fun ColumnScope.NeutralView(
onHistoryFile: (String) -> Unit,
onReset: (StatusEntry) -> Unit,
onDelete: (StatusEntry) -> Unit,
onOpenFileInFolder: (String?) -> Unit,
onAllAction: () -> Unit,
onAlternateShowAsTree: () -> Unit,
onTreeDirectoryClicked: (String) -> Unit,
Expand Down Expand Up @@ -548,6 +555,7 @@ fun ColumnScope.NeutralView(
onHistory = { onHistoryFile(statusEntry.filePath) },
onReset = { onReset(statusEntry) },
onDelete = { onDelete(statusEntry) },
onOpenFileInFolder = { onOpenFileInFolder(statusEntry.parentDirectoryPath) },
)
},
onAllAction = onAllAction,
Expand Down Expand Up @@ -588,6 +596,7 @@ fun ColumnScope.NeutralView(
onHistory = { onHistoryFile(statusEntry.filePath) },
onReset = { onReset(statusEntry) },
onDelete = { onDelete(statusEntry) },
onOpenFileInFolder = { onOpenFileInFolder(statusEntry.parentDirectoryPath) },
)
},
onAllAction = onAllAction,
Expand Down Expand Up @@ -1079,8 +1088,6 @@ fun EntriesHeader(
)
}



if (showSearch) {
SearchTextField(
searchFilter = searchFilter,
Expand All @@ -1097,7 +1104,6 @@ fun EntriesHeader(
requestFocus = false
}
}

}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.jetpackduba.gitnuro.ui.context_menu
import androidx.compose.foundation.ExperimentalFoundationApi
import com.jetpackduba.gitnuro.generated.resources.Res
import com.jetpackduba.gitnuro.generated.resources.blame
import com.jetpackduba.gitnuro.generated.resources.folder_open
import com.jetpackduba.gitnuro.generated.resources.history
import org.eclipse.jgit.diff.DiffEntry
import org.jetbrains.compose.resources.painterResource
Expand All @@ -12,6 +13,7 @@ fun committedChangesEntriesContextMenuItems(
diffEntry: DiffEntry,
onBlame: () -> Unit,
onHistory: () -> Unit,
onOpenFileInFolder: () -> Unit,
): List<ContextMenuElement> {
return mutableListOf<ContextMenuElement>().apply {
if (diffEntry.changeType != DiffEntry.ChangeType.ADD ||
Expand All @@ -31,6 +33,14 @@ fun committedChangesEntriesContextMenuItems(
onClick = onHistory,
)
)

add(
ContextMenuElement.ContextTextEntry(
label = "Open file in folder",
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use string resources for any new strings (The old ones are still being migrated)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was delighted to see that localization already started. I tried to use the resources but as this in a non composable context, I cannot simply access the resources. How should I do this? Hand down the Strings into the context menu?

Copy link
Owner

@JetpackDuba JetpackDuba Jul 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, it's a bit tricky out of the box. I've pushed some changes in the main branch to add proper support. If you rebase it, you will also see some examples as I extracted the rest of strings in that context menu.

icon = { painterResource(Res.drawable.folder_open) },
onClick = onOpenFileInFolder,
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ fun statusEntriesContextMenuItems(
onDelete: () -> Unit = {},
onBlame: () -> Unit,
onHistory: () -> Unit,
onOpenFileInFolder: () -> Unit,
): List<ContextMenuElement> {
return mutableListOf<ContextMenuElement>().apply {
if (statusEntry.statusType != StatusType.ADDED) {
Expand Down Expand Up @@ -54,6 +55,14 @@ fun statusEntriesContextMenuItems(
)
)
}

add(
ContextMenuElement.ContextTextEntry(
label = "Open file in folder",
icon = { painterResource(Res.drawable.folder_open) },
onClick = onOpenFileInFolder,
)
)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ package com.jetpackduba.gitnuro.viewmodels
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.ui.text.input.TextFieldValue
import com.jetpackduba.gitnuro.extensions.delayedStateChange
import com.jetpackduba.gitnuro.extensions.filePath
import com.jetpackduba.gitnuro.extensions.fullData
import com.jetpackduba.gitnuro.extensions.lowercaseContains
import com.jetpackduba.gitnuro.extensions.*
import com.jetpackduba.gitnuro.git.CloseableView
import com.jetpackduba.gitnuro.git.RefreshType
import com.jetpackduba.gitnuro.git.TabState
Expand All @@ -19,6 +16,7 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import org.eclipse.jgit.diff.DiffEntry
import org.eclipse.jgit.revwalk.RevCommit
import java.io.File
import javax.inject.Inject

private const val MIN_TIME_IN_MS_TO_SHOW_LOAD = 300L
Expand Down Expand Up @@ -159,6 +157,12 @@ class CommitChangesViewModel @Inject constructor(
appSettingsRepository.showChangesAsTree = !appSettingsRepository.showChangesAsTree
}

fun openFileInFolder(folderPath: String?) = tabState.runOperation(
refreshType = RefreshType.NONE,
) {
folderPath?.let { File(it).openFileInFolder() }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I forgot to mention it last time: Use a regular if statement instead of let, it's more readable when checking nullability of parameters

}

fun onDirectoryClicked(directoryPath: String) {
val contractedDirectories = treeContractedDirectories.value

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ class StatusViewModel @Inject constructor(
stageStateFiltered.unstaged,
contractedDirectories
) { it.filePath },
filteredStaged = entriesToTreeEntry(stageStateFiltered.filteredStaged, contractedDirectories) { it.filePath },
filteredStaged = entriesToTreeEntry(
stageStateFiltered.filteredStaged,
contractedDirectories
) { it.filePath },
filteredUnstaged = entriesToTreeEntry(
stageStateFiltered.filteredUnstaged,
contractedDirectories
Expand Down Expand Up @@ -509,6 +512,12 @@ class StatusViewModel @Inject constructor(
fileToDelete.deleteRecursively()
}

fun openFileInFolder(folderPath: String?) = tabState.runOperation(
refreshType = RefreshType.NONE,
) {
folderPath?.let { File(it).openFileInFolder() }
}

fun updateCommitMessage(message: String) {
savedCommitMessage = savedCommitMessage.copy(message = message)
persistMessage()
Expand Down
Loading