editor.go

  1package editor
  2
  3import (
  4	"context"
  5	"fmt"
  6	"io/fs"
  7	"log/slog"
  8	"math/rand"
  9	"net/http"
 10	"os"
 11	"os/exec"
 12	"path/filepath"
 13	"runtime"
 14	"slices"
 15	"strings"
 16	"unicode"
 17
 18	"github.com/charmbracelet/bubbles/v2/key"
 19	"github.com/charmbracelet/bubbles/v2/textarea"
 20	tea "github.com/charmbracelet/bubbletea/v2"
 21	"github.com/charmbracelet/crush/internal/app"
 22	"github.com/charmbracelet/crush/internal/config"
 23	"github.com/charmbracelet/crush/internal/fsext"
 24	"github.com/charmbracelet/crush/internal/message"
 25	"github.com/charmbracelet/crush/internal/session"
 26	"github.com/charmbracelet/crush/internal/tui/components/chat"
 27	"github.com/charmbracelet/crush/internal/tui/components/completions"
 28	"github.com/charmbracelet/crush/internal/tui/components/core/layout"
 29	"github.com/charmbracelet/crush/internal/tui/components/dialogs"
 30	"github.com/charmbracelet/crush/internal/tui/components/dialogs/commands"
 31	"github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
 32	"github.com/charmbracelet/crush/internal/tui/components/dialogs/quit"
 33	"github.com/charmbracelet/crush/internal/tui/styles"
 34	"github.com/charmbracelet/crush/internal/tui/util"
 35	"github.com/charmbracelet/lipgloss/v2"
 36)
 37
 38type Editor interface {
 39	util.Model
 40	layout.Sizeable
 41	layout.Focusable
 42	layout.Help
 43	layout.Positional
 44
 45	SetSession(session session.Session) tea.Cmd
 46	IsCompletionsOpen() bool
 47	HasAttachments() bool
 48	Cursor() *tea.Cursor
 49}
 50
 51type FileCompletionItem struct {
 52	Path string // The file path
 53}
 54
 55type editorCmp struct {
 56	width              int
 57	height             int
 58	x, y               int
 59	app                *app.App
 60	session            session.Session
 61	textarea           *textarea.Model
 62	attachments        []message.Attachment
 63	deleteMode         bool
 64	readyPlaceholder   string
 65	workingPlaceholder string
 66
 67	keyMap EditorKeyMap
 68
 69	// injected file dir lister
 70	listDirResolver fsext.DirectoryListerResolver
 71
 72	// File path completions
 73	currentQuery          string
 74	completionsStartIndex int
 75	isCompletionsOpen     bool
 76
 77	previouslyScrollingPromptHistory bool
 78	promptHistoryIndex               int
 79	historyCache                     []string
 80}
 81
 82var DeleteKeyMaps = DeleteAttachmentKeyMaps{
 83	AttachmentDeleteMode: key.NewBinding(
 84		key.WithKeys("ctrl+r"),
 85		key.WithHelp("ctrl+r+{i}", "delete attachment at index i"),
 86	),
 87	Escape: key.NewBinding(
 88		key.WithKeys("esc", "alt+esc"),
 89		key.WithHelp("esc", "cancel delete mode"),
 90	),
 91	DeleteAllAttachments: key.NewBinding(
 92		key.WithKeys("r"),
 93		key.WithHelp("ctrl+r+r", "delete all attachments"),
 94	),
 95}
 96
 97const (
 98	maxAttachments = 5
 99)
100
101type OpenEditorMsg struct {
102	Text string
103}
104
105func (m *editorCmp) openEditor(value string) tea.Cmd {
106	editor := os.Getenv("EDITOR")
107	if editor == "" {
108		// Use platform-appropriate default editor
109		if runtime.GOOS == "windows" {
110			editor = "notepad"
111		} else {
112			editor = "nvim"
113		}
114	}
115
116	tmpfile, err := os.CreateTemp("", "msg_*.md")
117	if err != nil {
118		return util.ReportError(err)
119	}
120	defer tmpfile.Close() //nolint:errcheck
121	if _, err := tmpfile.WriteString(value); err != nil {
122		return util.ReportError(err)
123	}
124	c := exec.CommandContext(context.TODO(), editor, tmpfile.Name())
125	c.Stdin = os.Stdin
126	c.Stdout = os.Stdout
127	c.Stderr = os.Stderr
128	return tea.ExecProcess(c, func(err error) tea.Msg {
129		if err != nil {
130			return util.ReportError(err)
131		}
132		content, err := os.ReadFile(tmpfile.Name())
133		if err != nil {
134			return util.ReportError(err)
135		}
136		if len(content) == 0 {
137			return util.ReportWarn("Message is empty")
138		}
139		os.Remove(tmpfile.Name())
140		return OpenEditorMsg{
141			Text: strings.TrimSpace(string(content)),
142		}
143	})
144}
145
146func (m *editorCmp) Init() tea.Cmd {
147	return nil
148}
149
150func (m *editorCmp) send() tea.Cmd {
151	defer func() {
152		m.resetHistory()
153	}()
154	value := m.textarea.Value()
155	value = strings.TrimSpace(value)
156
157	switch value {
158	case "exit", "quit":
159		m.textarea.Reset()
160		return util.CmdHandler(dialogs.OpenDialogMsg{Model: quit.NewQuitDialog()})
161	}
162
163	m.textarea.Reset()
164	attachments := m.attachments
165
166	m.attachments = nil
167	if value == "" {
168		return nil
169	}
170
171	// Change the placeholder when sending a new message.
172	m.randomizePlaceholders()
173
174	return tea.Batch(
175		util.CmdHandler(chat.SendMsg{
176			Text:        value,
177			Attachments: attachments,
178		}),
179	)
180}
181
182func (m *editorCmp) repositionCompletions() tea.Msg {
183	x, y := m.completionsPosition()
184	return completions.RepositionCompletionsMsg{X: x, Y: y}
185}
186
187func onCompletionItemSelect(fsys fs.FS, activeModelHasImageSupport func() (bool, string), item FileCompletionItem, insert bool, m *editorCmp) (tea.Model, tea.Cmd) {
188	var cmd tea.Cmd
189	path := item.Path
190	// check if item is an image
191	if isExtOfAllowedImageType(path) {
192		if imagesSupported, modelName := activeModelHasImageSupport(); !imagesSupported {
193			// TODO(tauraamui): consolidate this kind of standard image attachment related warning
194			return m, util.ReportWarn("File attachments are not supported by the current model: " + modelName)
195		}
196		slog.Debug("checking if image is too big", path, 1)
197		tooBig, _ := filepicker.IsFileTooBigWithFS(os.DirFS(filepath.Dir(path)), path, filepicker.MaxAttachmentSize)
198		if tooBig {
199			return m, nil
200		}
201
202		content, err := fs.ReadFile(fsys, path)
203		if err != nil {
204			return m, nil
205		}
206		mimeBufferSize := min(512, len(content))
207		mimeType := http.DetectContentType(content[:mimeBufferSize])
208		fileName := filepath.Base(path)
209		attachment := message.Attachment{FilePath: path, FileName: fileName, MimeType: mimeType, Content: content}
210		cmd = util.CmdHandler(filepicker.FilePickedMsg{
211			Attachment: attachment,
212		})
213	}
214
215	word := m.textarea.Word()
216	// If the selected item is a file, insert its path into the textarea
217	originalValue := m.textarea.Value()
218	newValue := originalValue[:m.completionsStartIndex] // Remove the current query
219	if cmd == nil {
220		newValue += path // insert the file path for non-images
221	}
222	newValue += originalValue[m.completionsStartIndex+len(word):] // Append the rest of the value
223	// XXX: This will always move the cursor to the end of the textarea.
224	m.textarea.SetValue(newValue)
225	m.textarea.MoveToEnd()
226	if !insert {
227		m.isCompletionsOpen = false
228		m.currentQuery = ""
229		m.completionsStartIndex = 0
230	}
231
232	return m, cmd
233}
234
235func isExtOfAllowedImageType(path string) bool {
236	isAllowedType := false
237	// TODO(tauraamui) [17/09/2025]: this needs to be combined with the actual data inference/checking
238	//                  of the contents that happens when we resolve the "mime" type
239	for _, ext := range filepicker.AllowedTypes {
240		if strings.HasSuffix(path, ext) {
241			isAllowedType = true
242			break
243		}
244	}
245	return isAllowedType
246}
247
248type ResolveAbs func(path string) (string, error)
249
250func activeModelHasImageSupport() (bool, string) {
251	agentCfg := config.Get().Agents["coder"]
252	model := config.Get().GetModelByType(agentCfg.Model)
253	return model.SupportsImages, model.Name
254}
255
256func (m *editorCmp) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
257	var cmd tea.Cmd
258	var cmds []tea.Cmd
259	switch msg := msg.(type) {
260	case tea.WindowSizeMsg:
261		return m, m.repositionCompletions
262	case filepicker.FilePickedMsg:
263		if len(m.attachments) >= maxAttachments {
264			return m, util.ReportError(fmt.Errorf("cannot add more than %d images", maxAttachments))
265		}
266		m.attachments = append(m.attachments, msg.Attachment)
267		return m, nil
268	case completions.CompletionsOpenedMsg:
269		m.isCompletionsOpen = true
270	case completions.CompletionsClosedMsg:
271		m.isCompletionsOpen = false
272		m.currentQuery = ""
273		m.completionsStartIndex = 0
274	case completions.SelectCompletionMsg:
275		if !m.isCompletionsOpen {
276			return m, nil
277		}
278		if item, ok := msg.Value.(FileCompletionItem); ok {
279			return onCompletionItemSelect(os.DirFS("."), activeModelHasImageSupport, item, msg.Insert, m)
280		}
281	case commands.OpenExternalEditorMsg:
282		if m.app.CoderAgent.IsSessionBusy(m.session.ID) {
283			return m, util.ReportWarn("Agent is working, please wait...")
284		}
285		return m, m.openEditor(m.textarea.Value())
286	case OpenEditorMsg:
287		m.textarea.SetValue(msg.Text)
288		m.textarea.MoveToEnd()
289	case tea.PasteMsg:
290		agentCfg := config.Get().Agents["coder"]
291		model := config.Get().GetModelByType(agentCfg.Model)
292		if !model.SupportsImages {
293			return m, util.ReportWarn("File attachments are not supported by the current model: " + model.Name)
294		}
295		return m, filepicker.OnPaste(filepicker.ResolveFS, string(msg)) // inject fsys accessibly from PWD
296
297	case commands.ToggleYoloModeMsg:
298		m.setEditorPrompt()
299		return m, nil
300	case tea.KeyPressMsg:
301		cur := m.textarea.Cursor()
302		curIdx := m.textarea.Width()*cur.Y + cur.X
303		switch {
304		// Completions
305		case msg.String() == "/" && !m.isCompletionsOpen &&
306			// only show if beginning of prompt, or if previous char is a space or newline:
307			(len(m.textarea.Value()) == 0 || unicode.IsSpace(rune(m.textarea.Value()[len(m.textarea.Value())-1]))):
308			m.isCompletionsOpen = true
309			m.currentQuery = ""
310			m.completionsStartIndex = curIdx
311
312			cmds = append(cmds, m.startCompletions())
313		case m.isCompletionsOpen && curIdx <= m.completionsStartIndex:
314			cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{}))
315		}
316		if key.Matches(msg, DeleteKeyMaps.AttachmentDeleteMode) {
317			m.deleteMode = true
318			return m, nil
319		}
320		if key.Matches(msg, DeleteKeyMaps.DeleteAllAttachments) && m.deleteMode {
321			m.deleteMode = false
322			m.attachments = nil
323			return m, nil
324		}
325		rune := msg.Code
326		if m.deleteMode && unicode.IsDigit(rune) {
327			num := int(rune - '0')
328			m.deleteMode = false
329			if num < 10 && len(m.attachments) > num {
330				if num == 0 {
331					m.attachments = m.attachments[num+1:]
332				} else {
333					m.attachments = slices.Delete(m.attachments, num, num+1)
334				}
335				return m, nil
336			}
337		}
338		if key.Matches(msg, m.keyMap.OpenEditor) {
339			if m.app.CoderAgent.IsSessionBusy(m.session.ID) {
340				return m, util.ReportWarn("Agent is working, please wait...")
341			}
342			return m, m.openEditor(m.textarea.Value())
343		}
344		if key.Matches(msg, DeleteKeyMaps.Escape) {
345			m.deleteMode = false
346			return m, nil
347		}
348		if key.Matches(msg, m.keyMap.Newline) {
349			m.textarea.InsertRune('\n')
350			cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{}))
351		}
352		// History
353		if key.Matches(msg, m.keyMap.Previous) || key.Matches(msg, m.keyMap.Next) {
354			m.textarea.SetValue(m.stepOverHistory(m.getUserMessagesAsText, m.getDirectionFromKey(msg)))
355		}
356		// Handle Enter key
357		if m.textarea.Focused() && key.Matches(msg, m.keyMap.SendMessage) {
358			value := m.textarea.Value()
359			if strings.HasSuffix(value, "\\") {
360				// If the last character is a backslash, remove it and add a newline.
361				m.textarea.SetValue(strings.TrimSuffix(value, "\\"))
362			} else {
363				// Otherwise, send the message
364				return m, m.send()
365			}
366		}
367	}
368
369	m.textarea, cmd = m.textarea.Update(msg)
370	cmds = append(cmds, cmd)
371
372	if m.textarea.Focused() {
373		kp, ok := msg.(tea.KeyPressMsg)
374		if ok {
375			if kp.String() == "space" || m.textarea.Value() == "" {
376				m.isCompletionsOpen = false
377				m.currentQuery = ""
378				m.completionsStartIndex = 0
379				cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{}))
380			} else {
381				word := m.textarea.Word()
382				if strings.HasPrefix(word, "/") {
383					// XXX: wont' work if editing in the middle of the field.
384					m.completionsStartIndex = strings.LastIndex(m.textarea.Value(), word)
385					m.currentQuery = word[1:]
386					x, y := m.completionsPosition()
387					x -= len(m.currentQuery)
388					m.isCompletionsOpen = true
389					cmds = append(cmds,
390						util.CmdHandler(completions.FilterCompletionsMsg{
391							Query:  m.currentQuery,
392							Reopen: m.isCompletionsOpen,
393							X:      x,
394							Y:      y,
395						}),
396					)
397				} else if m.isCompletionsOpen {
398					m.isCompletionsOpen = false
399					m.currentQuery = ""
400					m.completionsStartIndex = 0
401					cmds = append(cmds, util.CmdHandler(completions.CloseCompletionsMsg{}))
402				}
403			}
404		}
405	}
406
407	return m, tea.Batch(cmds...)
408}
409
410func (m *editorCmp) setEditorPrompt() {
411	if perm := m.app.Permissions; perm != nil {
412		if perm.SkipRequests() {
413			m.textarea.SetPromptFunc(4, yoloPromptFunc)
414			return
415		}
416	}
417	m.textarea.SetPromptFunc(4, normalPromptFunc)
418}
419
420func (m *editorCmp) completionsPosition() (int, int) {
421	cur := m.textarea.Cursor()
422	if cur == nil {
423		return m.x, m.y + 1 // adjust for padding
424	}
425	x := cur.X + m.x
426	y := cur.Y + m.y + 1 // adjust for padding
427	return x, y
428}
429
430func (m *editorCmp) Cursor() *tea.Cursor {
431	cursor := m.textarea.Cursor()
432	if cursor != nil {
433		cursor.X = cursor.X + m.x + 1
434		cursor.Y = cursor.Y + m.y + 1 // adjust for padding
435	}
436	return cursor
437}
438
439var readyPlaceholders = [...]string{
440	"Ready!",
441	"Ready...",
442	"Ready?",
443	"Ready for instructions",
444}
445
446var workingPlaceholders = [...]string{
447	"Working!",
448	"Working...",
449	"Brrrrr...",
450	"Prrrrrrrr...",
451	"Processing...",
452	"Thinking...",
453}
454
455func (m *editorCmp) randomizePlaceholders() {
456	m.workingPlaceholder = workingPlaceholders[rand.Intn(len(workingPlaceholders))]
457	m.readyPlaceholder = readyPlaceholders[rand.Intn(len(readyPlaceholders))]
458}
459
460func (m *editorCmp) View() string {
461	t := styles.CurrentTheme()
462	// Update placeholder
463	if m.app.CoderAgent != nil && m.app.CoderAgent.IsBusy() {
464		m.textarea.Placeholder = m.workingPlaceholder
465	} else {
466		m.textarea.Placeholder = m.readyPlaceholder
467	}
468	if m.app.Permissions.SkipRequests() {
469		m.textarea.Placeholder = "Yolo mode!"
470	}
471	if len(m.attachments) == 0 {
472		content := t.S().Base.Padding(1).Render(
473			m.textarea.View(),
474		)
475		return content
476	}
477	content := t.S().Base.Padding(0, 1, 1, 1).Render(
478		lipgloss.JoinVertical(lipgloss.Top,
479			m.attachmentsContent(),
480			m.textarea.View(),
481		),
482	)
483	return content
484}
485
486func (m *editorCmp) SetSize(width, height int) tea.Cmd {
487	m.width = width
488	m.height = height
489	m.textarea.SetWidth(width - 2)   // adjust for padding
490	m.textarea.SetHeight(height - 2) // adjust for padding
491	return nil
492}
493
494func (m *editorCmp) GetSize() (int, int) {
495	return m.textarea.Width(), m.textarea.Height()
496}
497
498func (m *editorCmp) attachmentsContent() string {
499	var styledAttachments []string
500	t := styles.CurrentTheme()
501	attachmentStyles := t.S().Base.
502		MarginLeft(1).
503		Background(t.FgMuted).
504		Foreground(t.FgBase)
505	for i, attachment := range m.attachments {
506		var filename string
507		if len(attachment.FileName) > 10 {
508			filename = fmt.Sprintf(" %s %s...", styles.DocumentIcon, attachment.FileName[0:7])
509		} else {
510			filename = fmt.Sprintf(" %s %s", styles.DocumentIcon, attachment.FileName)
511		}
512		if m.deleteMode {
513			filename = fmt.Sprintf("%d%s", i, filename)
514		}
515		styledAttachments = append(styledAttachments, attachmentStyles.Render(filename))
516	}
517	content := lipgloss.JoinHorizontal(lipgloss.Left, styledAttachments...)
518	return content
519}
520
521func (m *editorCmp) SetPosition(x, y int) tea.Cmd {
522	m.x = x
523	m.y = y
524	return nil
525}
526
527func (m *editorCmp) startCompletions() func() tea.Msg {
528	return func() tea.Msg {
529		files, _, _ := m.listDirResolver()(".", nil)
530		slices.Sort(files)
531		completionItems := make([]completions.Completion, 0, len(files))
532		for _, file := range files {
533			file = strings.TrimPrefix(file, "./")
534			completionItems = append(completionItems, completions.Completion{
535				Title: file,
536				Value: FileCompletionItem{
537					Path: file,
538				},
539			})
540		}
541
542		x, y := m.completionsPosition()
543		return completions.OpenCompletionsMsg{
544			Completions: completionItems,
545			X:           x,
546			Y:           y,
547		}
548	}
549}
550
551// Blur implements Container.
552func (c *editorCmp) Blur() tea.Cmd {
553	c.textarea.Blur()
554	return nil
555}
556
557// Focus implements Container.
558func (c *editorCmp) Focus() tea.Cmd {
559	return c.textarea.Focus()
560}
561
562// IsFocused implements Container.
563func (c *editorCmp) IsFocused() bool {
564	return c.textarea.Focused()
565}
566
567// Bindings implements Container.
568func (c *editorCmp) Bindings() []key.Binding {
569	return c.keyMap.KeyBindings()
570}
571
572// TODO: most likely we do not need to have the session here
573// we need to move some functionality to the page level
574func (c *editorCmp) SetSession(session session.Session) tea.Cmd {
575	c.session = session
576	return nil
577}
578
579func (c *editorCmp) IsCompletionsOpen() bool {
580	return c.isCompletionsOpen
581}
582
583func (c *editorCmp) HasAttachments() bool {
584	return len(c.attachments) > 0
585}
586
587func normalPromptFunc(info textarea.PromptInfo) string {
588	t := styles.CurrentTheme()
589	if info.LineNumber == 0 {
590		return "  > "
591	}
592	if info.Focused {
593		return t.S().Base.Foreground(t.GreenDark).Render("::: ")
594	}
595	return t.S().Muted.Render("::: ")
596}
597
598func yoloPromptFunc(info textarea.PromptInfo) string {
599	t := styles.CurrentTheme()
600	if info.LineNumber == 0 {
601		if info.Focused {
602			return fmt.Sprintf("%s ", t.YoloIconFocused)
603		} else {
604			return fmt.Sprintf("%s ", t.YoloIconBlurred)
605		}
606	}
607	if info.Focused {
608		return fmt.Sprintf("%s ", t.YoloDotsFocused)
609	}
610	return fmt.Sprintf("%s ", t.YoloDotsBlurred)
611}
612
613func (m *editorCmp) getUserMessagesAsText(ctx context.Context) ([]string, error) {
614	if len(m.historyCache) > 0 {
615		return m.historyCache, nil
616	}
617	allMessages, err := m.app.Messages.List(ctx, m.session.ID)
618	if err != nil {
619		return nil, err
620	}
621
622	var userMessages []string
623	for _, msg := range allMessages {
624		if msg.Role == message.User {
625			userMessages = append(userMessages, msg.Content().Text)
626		}
627	}
628
629	userMessages = append(userMessages, m.textarea.Value())
630	m.historyCache = userMessages
631	return userMessages, nil
632}
633
634type direction int
635
636const (
637	previous = iota
638	next
639)
640
641func (m *editorCmp) getDirectionFromKey(msg tea.KeyPressMsg) func() direction {
642	return func() direction {
643		if key.Matches(msg, m.keyMap.Previous) {
644			return previous
645		}
646		return next
647	}
648}
649
650func (m *editorCmp) stepOverHistory(resolveHistoricMessages func(context.Context) ([]string, error), resolveDirection func() direction) string {
651	// NOTE(tauraamui): the last entry in this list will be the current contents of the input field/box
652	messageHistory, err := resolveHistoricMessages(context.Background())
653	if err != nil {
654		return ""
655	}
656
657	// the list will/should always have at least the current message in the input in the list
658	if len(messageHistory) == 1 {
659		return messageHistory[0]
660	}
661
662	// the first time we invoke scroll we need to start from top of the list
663	if !m.previouslyScrollingPromptHistory {
664		m.promptHistoryIndex = len(messageHistory) - 1
665		m.previouslyScrollingPromptHistory = true
666	}
667
668	switch resolveDirection() {
669	case previous:
670		return m.stepBack(messageHistory)
671	case next:
672		return m.stepForward(messageHistory)
673	}
674	return ""
675}
676
677func (m *editorCmp) stepBack(history []string) string {
678	m.promptHistoryIndex -= 1
679	if m.promptHistoryIndex < 0 {
680		m.promptHistoryIndex = 0
681	}
682	return history[m.promptHistoryIndex]
683}
684
685func (m *editorCmp) stepForward(history []string) string {
686	m.promptHistoryIndex += 1
687	maxIndex := len(history) - 1
688	if m.promptHistoryIndex > maxIndex {
689		m.promptHistoryIndex = maxIndex
690	}
691	return history[m.promptHistoryIndex]
692}
693
694func (m *editorCmp) resetHistory() {
695	m.historyCache = nil
696	m.promptHistoryIndex = 0
697	m.previouslyScrollingPromptHistory = false
698}
699
700func newTextArea() *textarea.Model {
701	t := styles.CurrentTheme()
702	ta := textarea.New()
703	ta.SetStyles(t.S().TextArea)
704	ta.ShowLineNumbers = false
705	ta.CharLimit = -1
706	ta.SetVirtualCursor(false)
707	ta.Focus()
708	return ta
709}
710
711func newEditor(app *app.App, resolveDirLister fsext.DirectoryListerResolver) *editorCmp {
712	e := editorCmp{
713		// TODO: remove the app instance from here
714		app:             app,
715		textarea:        newTextArea(),
716		keyMap:          DefaultEditorKeyMap(),
717		listDirResolver: resolveDirLister,
718	}
719	e.setEditorPrompt()
720
721	e.randomizePlaceholders()
722	e.textarea.Placeholder = e.readyPlaceholder
723
724	return &e
725}
726
727func New(app *app.App) Editor {
728	ls := app.Config().Options.TUI.Completions.Limits
729	return newEditor(app, fsext.ResolveDirectoryLister(ls()))
730}