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
3 changes: 3 additions & 0 deletions apps/desktop/src/components/FileList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import { editPatch } from '$lib/editMode/editPatchUtils';
import { abbreviateFolders, changesToFileTree } from '$lib/files/filetreeV3';
import { type TreeChange, isExecutableStatus } from '$lib/hunks/change';
import { getFileType } from '@gitbutler/ui/components/file/fileTypeUtils';
import { MODE_SERVICE } from '$lib/mode/modeService';
import { showToast } from '$lib/notifications/toasts';
import { FILE_SELECTION_MANAGER } from '$lib/selection/fileSelectionManager.svelte';
Expand Down Expand Up @@ -221,6 +222,7 @@

{#snippet fileTemplate(change: TreeChange, idx: number, depth: number = 0)}
{@const isExecutable = isExecutableStatus(change.status)}
{@const fileType = getFileType(change.status)}
{@const selected = idSelection.has(change.path, selectionId)}
<FileListItemWrapper
{selectionId}
Expand All @@ -234,6 +236,7 @@
hideBorder={hideLastFileBorder && idx === visibleFiles.length - 1}
draggable={draggableFiles}
executable={isExecutable}
{fileType}
showCheckbox={showCheckboxes}
focusableOpts={{ onKeydown: (e) => handleKeyDown(change, idx, e), autoAction: true }}
onclick={(e) => {
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/components/FileListItemWrapper.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
linesRemoved?: number;
depth?: number;
executable?: boolean;
fileType?: 'regular' | 'executable' | 'symlink' | 'submodule';
focusableOpts?: FocusableOptions;
showCheckbox?: boolean;
draggable?: boolean;
Expand All @@ -58,6 +59,7 @@
listMode,
depth,
executable,
fileType,
showCheckbox,
conflictEntries,
draggable,
Expand Down Expand Up @@ -165,6 +167,7 @@
linesRemoved={lineChangesStat?.removed}
fileStatusTooltip={previousTooltipText}
{executable}
{fileType}
oncontextmenu={(e) => {
e.stopPropagation();
e.preventDefault();
Expand All @@ -186,6 +189,7 @@
indeterminate={checkStatus.current === 'indeterminate'}
{depth}
{executable}
{fileType}
draggable={!draggableDisabled}
{onkeydown}
{hideBorder}
Expand Down
91 changes: 82 additions & 9 deletions packages/ui/src/lib/components/file/FileIcon.svelte
Original file line number Diff line number Diff line change
@@ -1,26 +1,99 @@
<script lang="ts">
import { getFileIcon } from '$components/file/getFileIcon';
import { fileIcons } from '$components/file/fileIcons';
import { convertToBase64 } from '$lib/utils/convertToBase64';
import { pxToRem } from '$lib/utils/pxToRem';

interface Props {
fileName: string;
size?: number;
fileType?: 'regular' | 'executable' | 'symlink' | 'submodule';
/**
* @deprecated Use fileType prop instead
*/
executable?: boolean;
}

const { fileName, size = 16 }: Props = $props();
const { fileName, size = 16, fileType = 'regular', executable = false }: Props = $props();

// Determine the actual file type, with backward compatibility
const actualFileType = $derived(() => {
if (executable && fileType === 'regular') {
return 'executable';
}
return fileType;
});

// Get the appropriate icon based on file type
const iconSrc = $derived(() => {
const type = actualFileType();

// For symlinks and submodules, always use their specific icons
if (type === 'symlink') {
const icon = fileIcons['symlink'];
return `data:image/svg+xml;base64,${convertToBase64(icon)}`;
}

if (type === 'submodule') {
const icon = fileIcons['submodule'];
return `data:image/svg+xml;base64,${convertToBase64(icon)}`;
}

// For regular and executable files, use the filename-based icon
return getFileIcon(fileName);
});

// Determine if we should show the executable overlay
const showExecutableOverlay = $derived(() => {
const type = actualFileType();
return type === 'executable';
});

// Get the executable overlay icon
const executableOverlaySrc = $derived(() => {
if (!showExecutableOverlay()) return '';
const icon = fileIcons['executable-overlay'];
return `data:image/svg+xml;base64,${convertToBase64(icon)}`;
});
</script>

<img
draggable="false"
src={getFileIcon(fileName)}
alt=""
class="file-icon"
style:--file-icon-size="{pxToRem(size)}rem"
/>
<div class="file-icon-container" style:--file-icon-size="{pxToRem(size)}rem">
<img
draggable="false"
src={iconSrc()}
alt=""
class="file-icon"
/>

{#if showExecutableOverlay()}
<img
draggable="false"
src={executableOverlaySrc()}
alt="executable"
class="executable-overlay"
/>
{/if}
</div>

<style lang="postcss">
.file-icon {
.file-icon-container {
position: relative;
display: inline-block;
width: var(--file-icon-size);
height: var(--file-icon-size);
}

.file-icon {
width: 100%;
height: 100%;
}

.executable-overlay {
position: absolute;
top: -2px;
right: -2px;
width: 50%;
height: 50%;
z-index: 1;
}
</style>
9 changes: 3 additions & 6 deletions packages/ui/src/lib/components/file/FileListItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import Checkbox from '$components/Checkbox.svelte';
import Icon from '$components/Icon.svelte';
import Tooltip from '$components/Tooltip.svelte';
import ExecutableLabel from '$components/file/ExecutableLabel.svelte';
import FileIndent from '$components/file/FileIndent.svelte';
import FileName from '$components/file/FileName.svelte';
import FileStatusBadge from '$components/file/FileStatusBadge.svelte';
Expand Down Expand Up @@ -35,6 +34,7 @@
active?: boolean;
hideBorder?: boolean;
executable?: boolean;
fileType?: 'regular' | 'executable' | 'symlink' | 'submodule';
actionOpts?: FocusableOptions;
oncheckclick?: (e: MouseEvent) => void;
oncheck?: (
Expand Down Expand Up @@ -72,6 +72,7 @@
listMode = 'list',
depth,
executable,
fileType,
actionOpts,
oncheck,
oncheckclick,
Expand Down Expand Up @@ -130,7 +131,7 @@
</div>
{/if}

<FileName {filePath} hideFilePath={listMode === 'tree'} />
<FileName {filePath} hideFilePath={listMode === 'tree'} {fileType} {executable} />

<div class="file-list-item__details">
{#if locked}
Expand Down Expand Up @@ -171,10 +172,6 @@
</Tooltip>
{/if}

{#if executable}
<ExecutableLabel />
{/if}

{#if fileStatus}
<FileStatusBadge tooltip={fileStatusTooltip} status={fileStatus} style={fileStatusStyle} />
{/if}
Expand Down
9 changes: 7 additions & 2 deletions packages/ui/src/lib/components/file/FileName.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@
filePath: string;
hideFilePath?: boolean;
textSize?: '12' | '13';
fileType?: 'regular' | 'executable' | 'symlink' | 'submodule';
/**
* @deprecated Use fileType prop instead
*/
executable?: boolean;
}

let { filePath, textSize = '12', hideFilePath }: Props = $props();
let { filePath, textSize = '12', hideFilePath, fileType, executable }: Props = $props();
const fileNameAndPath = $derived(splitFilePath(filePath));
const filePathParts = $derived({
first: fileNameAndPath.path.split('/').slice(0, -1).join('/'),
Expand All @@ -18,7 +23,7 @@
</script>

<div class="file-name">
<FileIcon fileName={fileNameAndPath.filename} size={16} />
<FileIcon fileName={fileNameAndPath.filename} size={16} {fileType} {executable} />
<span class="text-{textSize} text-semibold file-name__name truncate">
{fileNameAndPath.filename}
</span>
Expand Down
9 changes: 3 additions & 6 deletions packages/ui/src/lib/components/file/FileViewHeader.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import Button from '$components/Button.svelte';
import Icon from '$components/Icon.svelte';
import LineStats from '$components/LineStats.svelte';
import ExecutableLabel from '$components/file/ExecutableLabel.svelte';
import FileName from '$components/file/FileName.svelte';
import FileStatusBadge from '$components/file/FileStatusBadge.svelte';
import type { FileStatus } from '$components/file/types';
Expand All @@ -18,6 +17,7 @@
linesRemoved?: number;
conflicted?: boolean;
executable?: boolean;
fileType?: 'regular' | 'executable' | 'symlink' | 'submodule';
transparent?: boolean;
noPaddings?: boolean;
oncontextmenu?: (e: MouseEvent) => void;
Expand All @@ -34,6 +34,7 @@
linesRemoved = 0,
conflicted,
executable,
fileType,
transparent,
noPaddings,
oncontextmenu,
Expand Down Expand Up @@ -63,18 +64,14 @@
{/if}

<div class="file-header__name">
<FileName {filePath} textSize="13" />
<FileName {filePath} textSize="13" {fileType} {executable} />
</div>

<div class="file-header__statuses">
{#if linesAdded > 0 || linesRemoved > 0}
<LineStats {linesAdded} {linesRemoved} />
{/if}

{#if executable}
<ExecutableLabel />
{/if}

{#if fileStatus}
<FileStatusBadge tooltip={fileStatusTooltip} status={fileStatus} style="full" />
{/if}
Expand Down
5 changes: 4 additions & 1 deletion packages/ui/src/lib/components/file/fileIcons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,5 +325,8 @@ export const fileIcons = {
xml: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle cx="5" cy="19" r="2" fill="#EA580C"/><mask id="a" width="20" height="22" x="4" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#D9D9D9" d="M4 0h20v22H4z"/></mask><g mask="url(#a)"><circle cx="5" cy="19" r="8" stroke="#EA580C" stroke-width="2"/></g><mask id="b" width="19" height="20" x="5" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#D9D9D9" d="M5 0h19v20H5z"/></mask><g mask="url(#b)"><circle cx="5" cy="19" r="15" stroke="#EA580C" stroke-width="2"/></g></svg>\n',
yaml: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#A78BFA" d="M6.533 5.864h2.755l2.654 5.011h.113l2.654-5.011h2.756l-4.245 7.522V17.5h-2.443v-4.114z"/></svg>\n',
yarn: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#60A5FA" fill-rule="evenodd" d="M5.724 20.594c-.175-.02-.35-.108-.526-.196a1 1 0 0 1-.201-.13q-.342-.288-.1-.773c0-.05.025-.1.05-.15s.05-.1.05-.151c-.401 0-.602-.2-.702-.502-.502-1.304-.402-2.308.602-3.31q.04-.081.078-.144c.069-.119.122-.212.122-.359 0-1.304.201-2.508 1.104-3.612a4.3 4.3 0 0 1 1.204-1.103c.2-.1.2-.201.1-.402a3.3 3.3 0 0 1-.702-1.605c-.08-.402.096-.803.273-1.204q.067-.15.128-.301c.1-.1.201-.201.301-.201.295-.074.535-.255.761-.426q.123-.095.243-.176c.702-.702 1.505-1.003 2.508-1.003.195 0 .295-.095.301-.192v-.01q.014-.056.032-.114.08-.269.22-.538a4 4 0 0 1 .35-.551l.301-.301c.154-.154.367-.19.503-.063l.007.007a.4.4 0 0 1 .092.156c.206.343.365.686.509.997l.012.025a14 14 0 0 0 .184.388l.004.007c.098.189.197.186.295.088.356-.152.531-.228.644-.173.11.053.16.229.259.575a9 9 0 0 1 .205 1.268c.137 1.648-.248 3.232-1.209 4.752-.05.1-.125.2-.2.3-.076.101-.15.202-.201.302-.1.2-.1.3.1.502a5.34 5.34 0 0 1 1.706 3.11 7 7 0 0 1 .058.564 8.4 8.4 0 0 1-.058 1.643q-.037.149-.037.23c0 .166.112.147.338.072a8 8 0 0 0 .552-.18A7.5 7.5 0 0 0 17 17.253a6 6 0 0 0 .439-.267l.254-.146c.427-.249.83-.483 1.281-.629a3 3 0 0 1 .572-.128c.104 0 .197-.012.285-.023l.072-.01q.087-.01.176-.011a.755.755 0 0 1 .771.747c0 .3-.2.502-.501.602a5 5 0 0 0-.803.229q-.139.05-.275.11a7 7 0 0 0-1.078.579q-.285.184-.553.386a9.7 9.7 0 0 1-1.71.943 12 12 0 0 1-1.602.562c-.1 0-.3.1-.401.201-.314.235-.628.287-.99.346q-.15.023-.314.055l-.255.024c-1.005.092-1.938.177-2.956.177l-.214-.001a1 1 0 0 1-.081.06l-.006-.009.03-.053-.067-.002a3.2 3.2 0 0 1-.666-.095 1.5 1.5 0 0 1-.234-.1 1.3 1.3 0 0 1-.167-.107 1 1 0 0 1-.199-.199.7.7 0 0 1-.102-.196q-.107-.43.092-.707l.024-.031a.8.8 0 0 1 .185-.165.6.6 0 0 0 .247-.054l.023-.01a.4.4 0 0 1 .131-.037.78.78 0 0 1-.4-.401c-.101-.1-.101-.1-.202 0a1 1 0 0 0-.05.124l-.001.002a2 2 0 0 0-.049.175c-.025.1-.05.2-.1.3-.144.48-.333.798-.6.979a1 1 0 0 1-.213.109 1.4 1.4 0 0 1-.523.075 3 3 0 0 1-.47-.058zm-.181 1.991a3.4 3.4 0 0 1-1.215-.385l-.025-.013c-.476-.238-1.102-.702-1.379-1.533a2.5 2.5 0 0 1-.115-.528 2.9 2.9 0 0 1-.394-.75c-.316-.831-.52-1.773-.358-2.774.14-.855.52-1.59 1.05-2.225.054-1.384.364-2.914 1.52-4.344q.205-.271.46-.543a6 6 0 0 1-.252-.978c-.14-.746.033-1.382.168-1.768a7 7 0 0 1 .25-.614c.04-.093.068-.155.094-.221a2 2 0 0 1 .45-.679c.04-.04.185-.187.358-.316.114-.086.382-.276.77-.384l.151-.112.004-.004.152-.112a5.2 5.2 0 0 1 2.627-1.35q.206-.372.461-.711.086-.114.186-.214l.301-.301c.425-.425 1.124-.806 1.974-.712.87.097 1.511.648 1.83 1.323q.07.12.136.246c.17.028.348.078.532.159.793.353 1.088 1.038 1.17 1.234a5.4 5.4 0 0 1 .226.707c.585 2.455.284 4.856-.986 7.099a7.4 7.4 0 0 1 1.385 3.104c.563-.314 1.326-.675 2.224-.787q.11-.014.22-.015h.002l.044-.006.032-.004.152-.018a2.9 2.9 0 0 1 1.233.13 2.75 2.75 0 0 1 1.87 2.6c0 1.451-1.065 2.23-1.87 2.5a2 2 0 0 1-.24.063c-.606.121-1.234.443-1.901.943l-.053.039c-1.186.83-2.55 1.403-3.782 1.754-.776.523-1.71.64-1.94.669l-.05.006q-.105.021-.211.031l-.266.024c-.991.091-2.012.185-3.126.185h-.023c-.32 0-.824 0-1.466-.16a2 2 0 0 1-.147-.043 3.5 3.5 0 0 1-.571-.25c-.6.16-1.186.122-1.662.038m3.508-1.41q.003 0-.001.001M15.304 3.73l.01.022q0-.003-.01-.022M6.498 8.38l.013-.006h-.002z" clip-rule="evenodd"/></svg>\n',
zig: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#FBBF24" fill-rule="evenodd" d="M11.306 14.857 20.4 4l-5.6 2.514H9.657L7.486 9.03h5.226L3.6 20l5.6-2.629h5.143l2.171-2.514z" clip-rule="evenodd"/></svg>\n'
zig: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#FBBF24" fill-rule="evenodd" d="M11.306 14.857 20.4 4l-5.6 2.514H9.657L7.486 9.03h5.226L3.6 20l5.6-2.629h5.143l2.171-2.514z" clip-rule="evenodd"/></svg>\n',
symlink: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path stroke="#06B6D4" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4M7.835 4.697a3.42 3.42 0 0 1 1.946-.806 3.42 3.42 0 0 1 3.138 2.097c.043.094.059.199.09.294V6a3 3 0 0 1 3 3v6a3 3 0 0 1-3 3H9a3 3 0 0 1-3-3v-6a3 3 0 0 1 1.835-2.757zM13 6v3"/><path stroke="#06B6D4" stroke-linecap="round" stroke-width="2" d="M16 11a2 2 0 0 0-2-2h-3a1 1 0 0 1-1-1V6a2 2 0 0 0-2-2h-.5"/></svg>\n',
submodule: '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><rect width="16" height="10" x="4" y="8" stroke="#F97316" stroke-width="2" rx="2"/><path fill="#F97316" d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><circle cx="12" cy="13" r="2" fill="#F97316"/><path stroke="#F97316" stroke-linecap="round" stroke-width="1.5" d="m9 13 1.5 1.5L15 10"/></svg>\n',
'executable-overlay': '<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 12 12"><circle cx="6" cy="6" r="5" fill="#16A34A" stroke="#fff" stroke-width="1"/><text x="6" y="8.5" fill="#fff" font-size="7" font-weight="bold" text-anchor="middle">x</text></svg>\n'
} as Record<string, string>;
85 changes: 85 additions & 0 deletions packages/ui/src/lib/components/file/fileTypeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/**
* Utilities for determining file types from TreeChange objects and ChangeState
*/

export type FileType = 'regular' | 'executable' | 'symlink' | 'submodule';

/**
* Determines the file type from a ChangeState object or status
*/
export function getFileType(status: any): FileType {
// Handle the different status types: Addition, Deletion, Modification, Rename
const state = getRelevantState(status);

if (!state || !state.kind) {
return 'regular';
}

switch (state.kind) {
case 'Link':
return 'symlink';
case 'Commit':
return 'submodule';
case 'BlobExecutable':
return 'executable';
case 'Blob':
default:
return 'regular';
}
}

/**
* Gets the relevant state from a status object
*/
function getRelevantState(status: any): any {
if (!status || !status.subject) {
return null;
}

const subject = status.subject;

// For additions, use the state
if (subject.state) {
return subject.state;
}

// For deletions, we might need the previous state
if (subject.previousState) {
return subject.previousState;
}

return null;
}

/**
* Checks if a file is executable (but not a symlink or submodule)
*/
export function isExecutableFile(status: any): boolean {
const fileType = getFileType(status);
return fileType === 'executable';
}

/**
* Checks if a file is a symlink
*/
export function isSymlink(status: any): boolean {
const fileType = getFileType(status);
return fileType === 'symlink';
}

/**
* Checks if a file is a submodule
*/
export function isSubmodule(status: any): boolean {
const fileType = getFileType(status);
return fileType === 'submodule';
}

/**
* Checks if a file should show an executable overlay
* (executable files that are not symlinks or submodules)
*/
export function shouldShowExecutableOverlay(status: any): boolean {
const fileType = getFileType(status);
return fileType === 'executable';
}
Loading