shell

package
v0.16.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 17, 2026 License: BSD-3-Clause Imports: 15 Imported by: 0

Documentation

Overview

Package shell holds the pure composition/model logic of the desktop shell: the launchable-app index, MIME "open with" resolution, the categorized application-menu model, the directory listing model and the thumbnail cache-key derivation. None of it touches a rendering surface, so every branch is unit-coverable against temp-dir fixtures; the render package turns these models into go-widgets widgets.

Index

Constants

View Source
const MimeDirectory = "inode/directory"

MimeDirectory is the MIME type the shell assigns to directory entries.

Variables

View Source
var (
	// ErrCopyExists is returned by CopyFile when an entry of the same name
	// already exists in a DIFFERENT destination directory: the copy is refused
	// rather than silently overwriting it, so the caller can offer a
	// confirm-overwrite dialog (then call CopyFileReplace). A SAME-directory
	// copy never returns this — it auto-suffixes (" copie") instead.
	ErrCopyExists = errors.New("copy: an item with that name already exists")
	// ErrCopyNotDir is returned when destDir exists but is not a directory.
	ErrCopyNotDir = errors.New("copy: destination is not a directory")
	// ErrCopyIntoSelf is returned when the copy would place a directory inside
	// its own subtree (an infinite recursion), e.g. copying /a into /a/b.
	ErrCopyIntoSelf = errors.New("copy: cannot copy a directory into itself")
)

Copy refusals: CopyFile returns one of these sentinel errors when it declines a copy for a structural reason (as opposed to an OS error it passes through).

View Source
var (
	// ErrMoveIntoSelf is returned when src already lives directly inside
	// destDir, so the move would be a no-op.
	ErrMoveIntoSelf = errors.New("move: source is already in the destination")
	// ErrMoveExists is returned when an entry of the same name already exists in
	// destDir: the move is refused rather than silently overwriting it.
	ErrMoveExists = errors.New("move: an item with that name already exists")
	// ErrMoveNotDir is returned when destDir exists but is not a directory.
	ErrMoveNotDir = errors.New("move: destination is not a directory")
)

Move refusals: MoveFile returns one of these sentinel errors when it declines a move for a structural reason (as opposed to an OS error it passes through).

Functions

func CopyFile added in v0.14.0

func CopyFile(src, destDir string) (string, error)

CopyFile copies the file (or directory) at src into destDir and returns the final destination path. Directories are copied recursively; every entry's permission bits are preserved.

Collisions are handled the way macOS's Finder does:

  • a SAME-directory copy (destDir is src's own parent) is never a refusal: the copy is written under a free " copie"-suffixed name (then " copie 2", …), so ⌘C/⌘V in place always duplicates;
  • a copy into a DIFFERENT directory that already holds an entry of src's base name returns ErrCopyExists (nothing written), so the caller can confirm an overwrite and, on confirmation, call CopyFileReplace.

It also refuses, without touching the filesystem, a destDir that is not a directory (ErrCopyNotDir) and a copy of a directory into its own subtree (ErrCopyIntoSelf). Any other filesystem error (a missing destDir, a permission denial, an unreadable source) is returned unwrapped — CopyFile never panics on one.

func CopyFileReplace added in v0.14.0

func CopyFileReplace(src, destDir string) (string, error)

CopyFileReplace is CopyFile's overwrite peer: it copies src into destDir under src's own base name, first removing any existing entry of that name (so a confirmed "Remplacer" overwrites it). It is the call a caller makes after a CopyFile reported ErrCopyExists and the user confirmed the replacement. A no-op self-copy (the computed destination is src itself) is left untouched.

func HumanBytes added in v0.11.0

func HumanBytes(n int64) string

HumanBytes formats a byte count as a compact human-readable string using French unit abbreviations (o, Ko, Mo, Go, To) and decimal (1000) steps, the convention macOS/Finder uses.

func IsImageName added in v0.10.0

func IsImageName(name string) bool

IsImageName reports whether name has a known image file extension. It is the single source of truth for extension-based image detection, shared by the thumbnail-eligibility policy here and the render layer's picture-glyph fallback, so the two never drift.

func MimeByExt added in v0.11.0

func MimeByExt(name string) string

MimeByExt maps a file name's extension to a MIME type by a small built-in table, or "" when the extension is not recognised. It covers the common image/video/audio/text/document kinds the file manager needs to pick an icon and a Type label; it never touches the filesystem.

func MoveFile added in v0.12.0

func MoveFile(src, destDir string) (string, error)

MoveFile moves the file (or directory) at src into destDir, preserving src's base name, and returns the final destination path.

On the same volume it is a single os.Rename; when the rename reports a cross-device error (EXDEV) it falls back to copying src's bytes into destDir and removing the original. It refuses, without touching the filesystem:

  • a no-op — src is already directly inside destDir (ErrMoveIntoSelf);
  • a name collision — destDir already holds an entry of src's base name (ErrMoveExists), so an existing file is never silently overwritten;
  • a destDir that exists but is not a directory (ErrMoveNotDir).

Any other filesystem error (a missing destDir, a permission denial on the rename/copy/remove, a source that cannot be read) is returned unwrapped so the caller can surface it to the user — MoveFile never panics on one.

func MoveFileReplace added in v0.14.0

func MoveFileReplace(src, destDir string) (string, error)

MoveFileReplace is MoveFile's overwrite peer: it removes any existing entry of src's base name in destDir, then moves src in under that name — so a confirmed "Remplacer"/"Déplacer" overwrites the collision. It delegates the move itself to MoveFile, inheriting its same-volume rename and cross-volume (EXDEV) copy+remove fallback and its destDir/not-a-directory checks. It refuses a no-op (src already directly inside destDir, ErrMoveIntoSelf) before removing anything, so a same-directory call can never delete the source. Other filesystem errors are returned unwrapped.

func Thumbnailable

func Thumbnailable(it FileItem) bool

Thumbnailable reports whether a file item should be given an image preview: a regular file that is an image, recognised either by its classified MIME type (image/*) or, when MIME classification is unavailable or weak, by a known image file extension — so a macOS .png classified as application/octet-stream is still thumbnailed. Directories and non-image files are never thumbnailed.

Types

type App

type App struct {
	ID          string
	Name        string
	GenericName string
	Comment     string
	Icon        string
	Exec        string
	Categories  []string
	Keywords    []string
	Terminal    bool
	// contains filtered or unexported fields
}

App is one launchable application, distilled from a desktop entry into the fields a dock / launcher needs. The originating entry is retained so the launcher can expand its Exec line at click time.

func (App) Entry

func (a App) Entry() *desktopentry.Entry

Entry returns the desktop entry this App was built from (nil for an App that was not derived from one). The launcher passes it to desktopentry.ExpandExec.

func (App) Label

func (a App) Label() string

Label is the user-visible name, falling back to the desktop-file id when the entry carries no Name.

type AppIndex

type AppIndex struct {
	// contains filtered or unexported fields
}

AppIndex is a sorted, de-duplicated, searchable set of launchable apps.

func NewAppIndex

func NewAppIndex(entries []*desktopentry.Entry) *AppIndex

NewAppIndex builds an index from scanned desktop entries. Non-launchable entries (wrong Type, no Exec) are dropped; the first entry seen for a given non-empty id wins (desktopentry.Scan already applies XDG directory precedence, so "first" means "highest priority"). Apps are ordered case-insensitively by their display label.

func (*AppIndex) All

func (x *AppIndex) All() []App

All returns a copy of the indexed apps in order.

func (*AppIndex) At

func (x *AppIndex) At(i int) App

At returns the app at index i.

func (*AppIndex) Len

func (x *AppIndex) Len() int

Len is the number of indexed apps.

func (*AppIndex) Search

func (x *AppIndex) Search(query string) []App

Search returns the apps matching query in index order. A blank (or whitespace-only) query returns every app.

type AppSource added in v0.4.0

type AppSource interface {
	// Apps is the sorted, de-duplicated, searchable launchable-app index that
	// feeds the dock and launcher.
	Apps() *AppIndex
	// Menu is the flattened, ordered application menu.
	Menu() *MenuModel
	// Dir is the current directory listing, already MIME-classified, that
	// feeds the file grid. It may be nil when no directory is available.
	Dir() *Dir
	// Resolve answers the "open with" query for a bare file name plus optional
	// leading content bytes: the classified MIME type, its default application
	// and the ordered candidate applications.
	Resolve(name string, content []byte) OpenWith
	// IconBytes returns the encoded image bytes (PNG/JPEG/GIF) for an icon
	// referenced by theme name (e.g. "web-browser") or by absolute path (e.g.
	// a thumbnail), and ok=false when it cannot be resolved. The render layer
	// decodes the bytes into a go-widgets Image (falling back to a placeholder
	// swatch on ok=false), so this is the single icon-pixel seam a source must
	// satisfy — no source touches the toolkit.
	IconBytes(name string) ([]byte, bool)
	// ThumbKey returns the IconBytes key of a file item's thumbnail — an
	// absolute cache path for the native source, a virtual asset name for the
	// embedded one — or "" when the item is not thumbnailed.
	ThumbKey(it FileItem) string
}

AppSource is the data source behind the desktop shell: everything the UI needs to render a populated desktop, abstracted away from where it comes from. It supplies the launchable-app index, the categorized application menu, the current directory listing (already MIME-classified), the "open with" resolution for a file, the encoded image bytes for an icon (by theme name or absolute path), and — for a file item — the icon-loader key of its thumbnail (or "" when it has none).

The two implementations live in github.com/go-widgets/desktop/source:

  • the native xdgSource scans a real XDG filesystem (desktopentry.Scan + icontheme + mime/mimeapps + menu.Load + go-thumbnail), exactly the behavior the shell has always had; and
  • the embeddedSource serves a curated set from an embed.FS, so the shell renders a real, populated desktop in the browser (js/wasm), where no real filesystem exists.

Both drive the identical shell/render composition logic — the whole point of the seam — so a Scene built from either is indistinguishable to the toolkit.

type Category

type Category struct {
	Name string
	Icon string
	Apps []App
}

Category is one branch of the application menu: a user-visible directory name (plus its icon) and the launchable apps allocated to it.

type ClipOp added in v0.14.0

type ClipOp int

ClipOp is the pending file-clipboard operation: nothing, a copy (⌘C/Ctrl+C) or a cut/move (⌘X/Ctrl+X). A Paste applies whichever op the clipboard holds.

const (
	// ClipNone is an empty clipboard: a paste is a no-op.
	ClipNone ClipOp = iota
	// ClipCopy marks the paths to be COPIED on paste (the originals stay).
	ClipCopy
	// ClipCut marks the paths to be MOVED on paste (the originals disappear).
	// Cut paths are drawn dimmed in the view (IsCut) until pasted or cleared.
	ClipCut
)

type Clipboard added in v0.14.0

type Clipboard struct {
	// contains filtered or unexported fields
}

Clipboard is the Finder's file clipboard: a small, filesystem-free record of which path(s) the user marked with ⌘C / ⌘X (Ctrl on Linux/Windows) and which operation to apply when they ⌘V. It is the pure state half of the copy/paste feature — the actual bytes move through CopyFile / MoveFile once a paste is classified by PasteKindFor — so the whole state machine is unit-testable without a window.

func (*Clipboard) Clear added in v0.14.0

func (c *Clipboard) Clear()

Clear empties the clipboard (after a move-paste consumes it, or on Escape).

func (*Clipboard) Empty added in v0.14.0

func (c *Clipboard) Empty() bool

Empty reports whether a paste would do nothing (no op, or no paths).

func (*Clipboard) IsCut added in v0.14.0

func (c *Clipboard) IsCut(path string) bool

IsCut reports whether path is currently marked for a MOVE (so the view draws it dimmed). It is false for a copy mark and for any path not on the clipboard.

func (*Clipboard) Op added in v0.14.0

func (c *Clipboard) Op() ClipOp

Op is the pending operation (ClipNone when empty).

func (*Clipboard) PasteKindFor added in v0.14.0

func (c *Clipboard) PasteKindFor(src, destDir string, forceMove bool) PasteKind

PasteKindFor classifies pasting src into destDir under the current clipboard op. forceMove requests a move regardless of the op (macOS ⌘⌥V, "Move Item Here"). It reads the filesystem only to test for a name collision; it writes nothing.

func (*Clipboard) Paths added in v0.14.0

func (c *Clipboard) Paths() []string

Paths is a copy of the marked paths (nil when empty), safe for the caller to keep or mutate.

func (*Clipboard) SetCopy added in v0.14.0

func (c *Clipboard) SetCopy(paths ...string)

SetCopy marks paths for copy-on-paste (⌘C). Blank paths are dropped; an all-blank/empty set clears the clipboard instead.

func (*Clipboard) SetCut added in v0.14.0

func (c *Clipboard) SetCut(paths ...string)

SetCut marks paths for move-on-paste (⌘X). Blank paths are dropped; an all-blank/empty set clears the clipboard instead.

type Dir

type Dir struct {
	Path  string
	Items []FileItem
}

Dir is a listed directory: its path and its items, directories first then files, each group ordered case-insensitively by name.

func ListDir

func ListDir(path string) (*Dir, error)

ListDir lists path into a Dir. Hidden entries (dotfiles: names beginning with ".", e.g. .ssh, .cache, .Trash) are skipped, so the file grid shows a clean user-facing directory rather than a wall of dotfile clutter. It does not classify MIME types (call Classify for that); a read failure (missing / unreadable directory) is returned as an error.

func (*Dir) Classify

func (d *Dir) Classify(r *Resolver)

Classify fills each item's Mime using r: directories get MimeDirectory, regular files are classified by name+content. A file that cannot be resolved (e.g. it became unreadable) is left with an empty Mime rather than aborting the whole listing.

func (*Dir) ClassifyLite added in v0.11.0

func (d *Dir) ClassifyLite()

ClassifyLite fills each item's Mime from its name/extension only, without the heavy content-sniffing Resolver — the portable classifier the file manager uses while navigating (a Resolver needs an XDG MIME database that does not exist on macOS/Windows or in the browser). Directories get MimeDirectory; regular files get an extension-derived MIME (or "" when the extension is unknown, which the Type column renders from the extension itself).

type FileItem

type FileItem struct {
	Name    string
	Path    string
	IsDir   bool
	Mime    string
	Size    int64     // byte size of a regular file (0 for directories)
	ModTime time.Time // last-modification time (zero when unavailable)
}

FileItem is one entry of a listed directory.

func (FileItem) HumanSize added in v0.11.0

func (it FileItem) HumanSize() string

HumanSize renders the item's byte size for the list view: an em dash for a directory (Finder shows "--" there), otherwise a compact human-readable size.

func (FileItem) IsImage added in v0.11.0

func (it FileItem) IsImage() bool

IsImage reports whether the item is a raster image the file manager can thumbnail (by MIME class when classified, else by name extension).

func (FileItem) ModTimeString added in v0.11.0

func (it FileItem) ModTimeString() string

ModTimeString formats the item's modification time for the list view's Date column ("--" when the time is unavailable).

func (FileItem) TypeLabel added in v0.11.0

func (it FileItem) TypeLabel() string

TypeLabel is the human-readable kind shown in the list view's Type column: "Dossier" for a directory, otherwise a short label derived from the MIME type (when classified) or the file-name extension.

type MenuModel struct {
	Categories []Category
}

MenuModel is the desktop application menu flattened into an ordered list of non-empty categories, ready to render as a go-widgets Menu / MenuBar. Nested submenus are walked depth-first, so a category appears once per menu node that carries apps, in menu (layout) order.

func NewMenuModel

func NewMenuModel(tree *menu.Tree) *MenuModel

NewMenuModel flattens a resolved menu.Tree. A nil tree (or a tree with a nil Root) yields an empty model. Only menu nodes that actually contain apps become categories; a purely structural submenu with no direct apps is skipped but still descended into.

func (m *MenuModel) Len() int

Len is the number of non-empty categories.

type OpenWith

type OpenWith struct {
	MimeType   string
	Default    App
	HasDefault bool
	Candidates []App
}

OpenWith is the resolved "open with" answer for a path or MIME type: the classified MIME type, the default application (if any) and the ordered list of candidate applications.

type PasteKind added in v0.14.0

type PasteKind int

PasteKind classifies how a single clipboard entry will paste into a target directory, so the UI knows whether to proceed silently, confirm a move, or confirm an overwrite — WITHOUT mutating the filesystem.

const (
	// PasteNoop: nothing to do (empty clipboard, or a move whose source is
	// already in the target directory).
	PasteNoop PasteKind = iota
	// PasteCopyPlain: a copy into another directory with no name collision —
	// proceed silently (macOS copies without a modal), then show a brief result.
	PasteCopyPlain
	// PasteCopySuffix: a copy back into the source's own directory — write a
	// " copie"-suffixed duplicate, no modal.
	PasteCopySuffix
	// PasteMoveConfirm: a move into another directory with no collision —
	// confirm "Déplacer « X » ici ?" before mutating.
	PasteMoveConfirm
	// PasteOverwriteConfirm: the target directory already holds an entry of the
	// source's name — confirm "Remplacer « X » ?" before overwriting.
	PasteOverwriteConfirm
)

type Place added in v0.11.0

type Place struct {
	Label string
	Path  string
	Kind  PlaceKind
}

Place is one sidebar row: a display label, the directory it navigates to (empty when the kind is not navigable, e.g. the network) and its kind.

func (Place) Navigable added in v0.11.0

func (p Place) Navigable() bool

Navigable reports whether clicking the place opens a real directory listing. The network location is not navigable — it shows the empty-shares state.

type PlaceKind added in v0.11.0

type PlaceKind int

PlaceKind classifies a sidebar entry so the render layer can pick an icon and so navigation can special-case the non-directory kinds (the network location has no path and shows an honest empty state).

const (
	// PlaceFolder is an ordinary navigable directory (a Favoris entry).
	PlaceFolder PlaceKind = iota
	// PlaceApplications is the applications folder (a Favoris entry).
	PlaceApplications
	// PlacePictures / PlaceMovies / PlaceMusic / PlaceDownloads are the media
	// Favoris entries, split out so each gets a distinct sidebar glyph.
	PlacePictures
	PlaceMovies
	PlaceMusic
	PlaceDownloads
	PlaceDesktop
	PlaceDocuments
	// PlaceHome is the user's home directory (an Emplacements entry).
	PlaceHome
	// PlaceVolume is the startup volume / root drive (an Emplacements entry).
	PlaceVolume
	// PlaceNetwork is the network location: there is no real network browsing
	// in a pure-Go shell, so it navigates to an honest empty "Aucun partage"
	// state rather than a fake listing. Its Path is "".
	PlaceNetwork
	// PlaceTrash is the trash / recycle bin (an Emplacements entry).
	PlaceTrash
)

type Places added in v0.11.0

type Places struct {
	Favorites []Place
	Locations []Place
}

Places is the file manager's sidebar model: the two labelled sections a macOS Finder shows — Favoris (user folders) and Emplacements (home, the startup volume, the network and the trash).

func DefaultPlaces added in v0.11.0

func DefaultPlaces() *Places

DefaultPlaces resolves the sidebar model for the running OS: real per-user paths on macOS (~/Desktop, ~/Documents, /Applications, ~/Pictures, ~/Movies, ~/Music, ~/Downloads; "Macintosh HD" -> /, ~/.Trash), the equivalent XDG user-dirs on Linux, and the known folders on Windows. When the home directory cannot be resolved (notably the js/wasm browser build, which has no real filesystem) it returns a minimal, non-crashing model so the sidebar still renders and the shell still builds.

type Resolver

type Resolver struct {
	// contains filtered or unexported fields
}

Resolver answers MIME classification and application-association queries by combining a shared MIME-info database with a mimeapps.list resolver.

func NewResolver

func NewResolver(db *mime.Database, apps *mimeapps.Resolver) *Resolver

NewResolver wires a MIME database and an application-association resolver into a Resolver.

func (*Resolver) ResolveName

func (r *Resolver) ResolveName(name string, content []byte) OpenWith

ResolveName classifies a bare name plus optional content bytes and resolves the associated applications. It performs no I/O, so it is the pure seam the path-based helper builds on.

func (*Resolver) ResolvePath

func (r *Resolver) ResolvePath(path string) (OpenWith, error)

ResolvePath classifies path by both name and content and resolves its associated applications. Directories (and other non-regular paths) are classified by name only. A stat failure (e.g. the path does not exist) or a content-read failure is returned as an error.

type Thumbnailer

type Thumbnailer struct {
	// contains filtered or unexported fields
}

Thumbnailer derives the freedesktop Thumbnail Managing Standard cache keys and paths for a file grid. It delegates the URI canonicalization, MD5 hash, cache-path layout and the actual decode+downscale+cache to github.com/go-thumbnail/thumbnail (the owner of that mechanism) and adds only the shell's policy: which items are thumbnailed and under which stable key.

func NewThumbnailer

func NewThumbnailer(size thumbnail.Size) *Thumbnailer

NewThumbnailer builds a Thumbnailer for the given standard thumbnail size.

func (*Thumbnailer) Ensure added in v0.10.0

func (t *Thumbnailer) Ensure(it FileItem) string

Ensure generates (or refreshes) the item's thumbnail through the go-thumbnail cache — decode the source image, downscale it into the standard size bucket and write the freedesktop-keyed PNG under $XDG_CACHE_HOME/thumbnails — and returns its on-disk cache path (the IconBytes key the render layer decodes), or "" when the item is not thumbnailable or the thumbnail could not be produced (an unreadable or undecodable source). The outcome is memoized per source path, positive and negative, so a repeated call is a map lookup.

func (*Thumbnailer) Key

func (t *Thumbnailer) Key(it FileItem) string

Key returns the stable cache key (the MD5 hash of the file's canonical file:// URI) for a thumbnailable item, or "" when the item is not thumbnailable. Two items with the same path always produce the same key.

func (*Thumbnailer) Path

func (t *Thumbnailer) Path(it FileItem) string

Path returns the on-disk cache path where the item's thumbnail lives (or would be generated), or "" for a non-thumbnailable item.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL