attach

package
v0.0.8 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package attach is the Ctrl+T file picker: a path being typed, the directory it names, and what is in it.

It is the command palette's twin rather than the dialog's. Same fixed width, same anchor, same selection marker, same key-hint footer, no buttons anywhere — the palette collects a command and this collects a path, and everything on screen is derived from what has been typed the way a shell derives completions. It replaced the last centred box with an OK button in the client; see docs/tui-2.0.md, divergence 49.

The package does no sending. It reports a path and a send mode; the app stages them on the composer.

Index

Constants

View Source
const Width = 60

Width is the overlay's fixed width in cells, the palette's exactly. Fixed for the palette's reason: a 200-column terminal should not stretch a listing of short filenames across the whole screen.

Variables

This section is empty.

Functions

func IsImage

func IsImage(name string) bool

IsImage reports whether this client would send the file as a photo.

It defers to the clipboard's answer rather than keeping a second list. Ctrl+V has been sending images for several releases and that list is the one proven against the send path — it excludes TIFF on purpose, because Telegram's InputMediaUploadedPhoto rejects it, and a picker that offered photo mode for a .tif would promise a send that fails.

Exported because a file dropped on the composer with no picker open is staged directly, and it has to send the same way it would have if it had been picked.

func ResolvePath

func ResolvePath(text string) (string, bool)

ResolvePath is the whole answer to "is this paste a file, and which one": the path a dropped or pasted string names, ready to hand to the composer, and whether it unambiguously names one at all.

One function rather than a predicate beside a converter. The two were separate for one round and the app used the predicate's answer with the converter's output — so a "~/shot.png" drop passed the check, which expands the tilde, and staged the literal string, which does not exist. A question and its answer computed by different code is a pair that can disagree.

The test is "unambiguously a path" rather than "possibly a path", because a paste that merely resembles one must not silently become an attachment instead of the message somebody meant to send: one line, rooted or home-relative or a URL, naming a file that is actually there.

func UnquotePath

func UnquotePath(text string) string

UnquotePath turns what a terminal delivers when a file is dropped on it into a path a stat call will find.

Dragging a file onto a terminal is how people already hand a path to a program, and it is the natural gesture for this surface — typing a path is the part nobody wants to do. But a drop does not arrive as keystrokes: the terminal pastes it, escaped the way a SHELL would need it, because what it is really doing is typing a command line for you. Three spellings are in use:

/Users/a/My\ Files/x.png     iTerm2, Terminal.app — backslash-escaped
'/Users/a/My Files/x.png'    quoted, single or double
file:///Users/a/My%20Files/  a URL, from several Linux terminals

Getting this wrong fails on exactly the files people drag: the ones with a space in the name. Everything else already works by being typed.

A multi-file drop delivers several paths separated by spaces, which is ambiguous against a single path that contains one. Only the first is taken, and only where the separation is unambiguous — a URL list — which is the honest reading while decision 5 keeps one staged attachment.

Types

type Action

type Action int

Action is what a keypress asked the app to do.

const (
	// ActionNone means the picker handled the key itself.
	ActionNone Action = iota
	// ActionAttach means Enter landed on a file: read Chosen.
	ActionAttach
	// ActionCancel means Escape was pressed: close, staging nothing.
	ActionCancel
)

type Entry

type Entry struct {
	// Name is the entry's own name. A directory carries no trailing slash
	// here — the view adds one, and completion adds one, but a name that
	// sometimes ends in a separator is a name every comparison has to
	// remember to normalise.
	Name string

	// Dir is set for a directory, INCLUDING a symlink that resolves to one:
	// what matters here is whether Enter can go into it. A broken link is
	// not a directory, because nothing can be entered.
	Dir bool

	// Size is the file's size in bytes; Items is a directory's entry count,
	// -1 when the directory could not be read to count them, and 0 before
	// anything has tried (see countInto, which only counts what is drawn).
	Size  int64
	Items int

	// ModTime is what the mtime column shows.
	ModTime time.Time

	// Image is set for a file this client would send as a photo. It is the
	// only thing the send-mode toggle applies to.
	Image bool
	// contains filtered or unexported fields
}

Entry is one row of a directory listing.

type Model

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

Model is the picker overlay.

func New

func New(r theme.Roles) Model

func (Model) AsPhoto

func (m Model) AsPhoto() bool

AsPhoto reports how the cursored file would send.

An image sends as a photo unless the reader said otherwise, which is the defect this component was built to fix: the prompt it replaces passed a hardcoded false, so Ctrl+T always attached as a document while Ctrl+V attached the same image as a photo. Anything that is not an image can only be a document, and the toggle says so rather than lying.

func (Model) Below

func (m Model) Below() int

Below is how many matches sit under the drawn window.

func (Model) Chosen

func (m Model) Chosen() (path string, asPhoto bool, ok bool)

Chosen is the absolute path of the cursored file and how to send it. ok is false unless the cursor is on a file — a directory is somewhere to go, not something to attach.

func (*Model) Close

func (m *Model) Close()

Close hides the picker, keeping the directory for next time.

It resets nothing else, because Open resets everything else: the path, the cursor and the send mode are all set from m.dir on the way back in. Doing it here as well would be two places that have to agree about what a fresh picker looks like, and mutation testing could not tell them apart — which is the tell that one of them was not doing anything.

func (Model) IsVisible

func (m Model) IsVisible() bool

func (Model) Matches

func (m Model) Matches() []Entry

Matches returns the filtered listing, for tests.

func (*Model) Open

func (m *Model) Open(fallback string)

Open shows the picker.

fallback is where to begin when the picker has nowhere else to be — the configured download directory, so the place the client saves to is the place it offers back. It is a FALLBACK and not a destination: once the picker has been somewhere, that is where it reopens, because attaching three files from one folder should not mean walking there three times. A caller wanting to move it should hand the path to Paste.

func (Model) Paste

func (m Model) Paste(text string) Model

Paste replaces the path with a dropped or pasted one.

Replaces rather than appends: a dropped path is absolute and complete, and appending it to whatever was already typed produces a path that exists nowhere. See UnquotePath for what a terminal actually delivers.

func (Model) Selected

func (m Model) Selected() (Entry, bool)

Selected returns the cursored entry, if the filter matched anything.

func (*Model) SetRoles

func (m *Model) SetRoles(r theme.Roles)

SetRoles supplies the TUI 2.0 semantic palette.

func (Model) Typed

func (m Model) Typed() string

Typed returns the path as entered, for tests and for the app's notices.

func (Model) Update

func (m Model) Update(msg tea.KeyPressMsg) (Model, Action)

Update handles a keypress while the picker owns input.

Movement is the arrows and nothing else, which is the palette's rule and the palette's reason: this is a text surface, so every printable key has to reach the path or a file called "jack" could not be typed. One spelling per action — the emacs chords were taken off the palette for being a second way to do one thing, and adding them back here would put the pair straight back out of step.

func (Model) View

func (m Model) View() string

View renders the overlay. Every content line is exactly Width cells, so the caller can place it without the frame shearing.

func (Model) Window

func (m Model) Window() (rows []Entry, top int)

Window is the slice of the match list currently drawn, and where in the match list it starts.

The listing scrolls rather than being capped: a cursor that can walk past the last drawn row is a cursor the reader cannot see, on a file Enter would still attach.

Jump to

Keyboard shortcuts

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