9fans.net/go@v0.0.7/cmd/sam/sam.go (about)

     1  // #include "sam.h"
     2  
     3  // Sam is a multi-file text editor.
     4  // This is a Go port of the original C version.
     5  // See https://9fans.github.io/plan9port/man/man1/sam.html
     6  // and https://9p.io/sys/doc/sam/sam.pdf for details.
     7  package main
     8  
     9  import (
    10  	"flag"
    11  	"fmt"
    12  	"io"
    13  	"os"
    14  	"path/filepath"
    15  )
    16  
    17  var genbuf [BLOCKSIZE]rune
    18  var genbuf2 [BLOCKSIZE]rune
    19  
    20  var iofile IOFile
    21  
    22  type IOFile interface {
    23  	io.ReadWriteCloser
    24  	Stat() (os.FileInfo, error)
    25  }
    26  
    27  var panicking int
    28  var rescuing int
    29  var genstr String
    30  var rhs String
    31  var curwd String
    32  var cmdstr String
    33  var empty []rune
    34  var curfile *File
    35  var flist *File
    36  var cmd *File
    37  var mainloop int
    38  var tempfile []*File
    39  var quitok bool = true
    40  var downloaded bool
    41  var dflag bool
    42  var Rflag bool
    43  var machine string
    44  var home string
    45  var bpipeok bool
    46  var termlocked int
    47  var samterm string = SAMTERM
    48  var rsamname string = RSAM
    49  var lastfile *File
    50  var disk *Disk
    51  var seq int
    52  
    53  var winsize string
    54  
    55  var baddir = [9]rune{'<', 'b', 'a', 'd', 'd', 'i', 'r', '>', '\n'}
    56  
    57  /* extern  */
    58  
    59  func main() {
    60  	var aflag bool
    61  	var Wflag string
    62  
    63  	flag.BoolVar(&Dflag, "D", Dflag, "-D") // debug
    64  	flag.BoolVar(&dflag, "d", dflag, "-d")
    65  	flag.BoolVar(&Rflag, "R", Rflag, "-R")
    66  	flag.StringVar(&machine, "r", machine, "-r")
    67  	flag.StringVar(&samterm, "t", samterm, "-t")
    68  	flag.StringVar(&rsamname, "s", rsamname, "-s")
    69  	flag.BoolVar(&aflag, "a", aflag, "-a (for samterm)")
    70  	flag.StringVar(&Wflag, "W", Wflag, "-W (for samterm)")
    71  
    72  	flag.Usage = usage
    73  	flag.Parse()
    74  
    75  	termargs := []string{"samterm"}
    76  	if aflag {
    77  		termargs = append(termargs, "-a")
    78  	}
    79  	if Wflag != "" {
    80  		termargs = append(termargs, "-W", Wflag)
    81  	}
    82  
    83  	Strinit(&cmdstr)
    84  	Strinit0(&lastpat)
    85  	Strinit0(&lastregexp)
    86  	Strinit0(&genstr)
    87  	Strinit0(&rhs)
    88  	Strinit0(&curwd)
    89  	Strinit0(&plan9cmd)
    90  	home, _ = os.UserHomeDir()
    91  	disk = diskinit()
    92  	if home == "" {
    93  		home = "/"
    94  	}
    95  	fileargs := flag.Args()
    96  	if !dflag {
    97  		startup(machine, Rflag, termargs, fileargs)
    98  	}
    99  	siginit()
   100  	getcurwd()
   101  	if len(fileargs) > 0 {
   102  		for i := 0; i < len(fileargs); i++ {
   103  			func() {
   104  				defer func() {
   105  					e := recover()
   106  					if e == nil || e == &mainloop {
   107  						return
   108  					}
   109  					panic(e)
   110  				}()
   111  
   112  				t := tmpcstr(fileargs[i])
   113  				Strduplstr(&genstr, t)
   114  				freetmpstr(t)
   115  				fixname(&genstr)
   116  				logsetname(newfile(), &genstr)
   117  			}()
   118  		}
   119  	} else if !downloaded {
   120  		newfile()
   121  	}
   122  	seq++
   123  	if len(file) > 0 {
   124  		current(file[0])
   125  	}
   126  
   127  	for {
   128  		func() {
   129  			defer func() {
   130  				e := recover()
   131  				if e == nil || e == &mainloop {
   132  					return
   133  				}
   134  				panic(e)
   135  			}()
   136  			cmdloop()
   137  			trytoquit() /* if we already q'ed, quitok will be TRUE */
   138  			os.Exit(0)
   139  		}()
   140  	}
   141  }
   142  
   143  func usage() {
   144  	dprint("usage: sam [-d] [-t samterm] [-s sam name] [-r machine] [file ...]\n")
   145  	os.Exit(2)
   146  }
   147  
   148  func rescue() {
   149  	nblank := 0
   150  	if rescuing++; rescuing > 1 {
   151  		return
   152  	}
   153  	iofile = nil
   154  	for _, f := range file {
   155  		if f == cmd || f.b.nc == 0 || !fileisdirty(f) {
   156  			continue
   157  		}
   158  		if iofile == nil {
   159  			var err error
   160  			iofile, err = os.OpenFile(filepath.Join(home, "sam.save"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0777)
   161  			if err != nil {
   162  				return
   163  			}
   164  		}
   165  		var buf string
   166  		if len(f.name.s) > 0 {
   167  			buf = string(f.name.s)
   168  		} else {
   169  			buf = fmt.Sprintf("nameless.%d", nblank)
   170  			nblank++
   171  		}
   172  		root := os.Getenv("PLAN9")
   173  		if root == "" {
   174  			root = "/usr/local/plan9"
   175  		}
   176  		fmt.Fprintf(iofile, "#!/bin/sh\n%s/bin/samsave '%s' $* <<'---%s'\n", root, buf, buf)
   177  		addr.r.p1 = 0
   178  		addr.r.p2 = f.b.nc
   179  		writeio(f)
   180  		fmt.Fprintf(iofile, "\n---%s\n", (string)(buf))
   181  	}
   182  }
   183  
   184  func panic_(format string, args ...interface{}) {
   185  	if panicking++; panicking == 1 {
   186  		s := fmt.Sprintf(format, args...)
   187  		func() {
   188  			defer func() {
   189  				e := recover()
   190  				if e == nil || e == &mainloop {
   191  					return
   192  				}
   193  				panic(e)
   194  			}()
   195  
   196  			wasd := downloaded
   197  			downloaded = false
   198  			dprint("sam: panic: %s\n", s)
   199  			if wasd {
   200  				fmt.Fprintf(os.Stderr, "sam: panic: %s\n", s)
   201  			}
   202  			rescue()
   203  			panic("abort")
   204  		}()
   205  	}
   206  }
   207  
   208  func hiccough(s string) {
   209  	if rescuing != 0 {
   210  		os.Exit(1)
   211  	}
   212  	if s != "" {
   213  		dprint("%s\n", s)
   214  		// panic(s) // TODO(rsc)
   215  	}
   216  	resetcmd()
   217  	resetxec()
   218  	resetsys()
   219  	if iofile != nil {
   220  		iofile.Close()
   221  	}
   222  
   223  	/*
   224  	 * back out any logged changes & restore old sequences
   225  	 */
   226  	for _, f := range file {
   227  		if f == cmd {
   228  			continue
   229  		}
   230  		if f.seq == seq {
   231  			bufdelete(&f.epsilon, 0, f.epsilon.nc)
   232  			f.seq = f.prevseq
   233  			f.dot.r = f.prevdot
   234  			f.mark = f.prevmark
   235  			m := Clean
   236  			if f.prevmod {
   237  				m = Dirty
   238  			}
   239  			state(f, m)
   240  		}
   241  	}
   242  
   243  	update()
   244  	if curfile != nil {
   245  		if curfile.unread {
   246  			curfile.unread = false
   247  		} else if downloaded {
   248  			outTs(Hcurrent, curfile.tag)
   249  		}
   250  	}
   251  	panic(&mainloop)
   252  }
   253  
   254  func intr() {
   255  	error_(Eintr)
   256  }
   257  
   258  func trytoclose(f *File) {
   259  	if f == cmd { /* possible? */
   260  		return
   261  	}
   262  	if f.deleted {
   263  		return
   264  	}
   265  	if fileisdirty(f) && !f.closeok {
   266  		f.closeok = true
   267  		var buf string
   268  		if len(f.name.s) > 0 {
   269  			buf = string(f.name.s)
   270  		} else {
   271  			buf = "nameless file"
   272  		}
   273  		error_s(Emodified, buf)
   274  	}
   275  	f.deleted = true
   276  }
   277  
   278  func trytoquit() {
   279  	if !quitok {
   280  		for _, f := range file {
   281  			if f != cmd && fileisdirty(f) {
   282  				quitok = true
   283  				eof = false
   284  				error_(Echanges)
   285  			}
   286  		}
   287  	}
   288  }
   289  
   290  func load(f *File) {
   291  	Strduplstr(&genstr, &f.name)
   292  	filename(f)
   293  	if len(f.name.s) > 0 {
   294  		saveaddr := addr
   295  		edit(f, 'I')
   296  		addr = saveaddr
   297  	} else {
   298  		f.unread = false
   299  		f.cleanseq = f.seq
   300  	}
   301  
   302  	fileupdate(f, true, true)
   303  }
   304  
   305  func cmdupdate() {
   306  	if cmd != nil && cmd.seq != 0 {
   307  		fileupdate(cmd, false, downloaded)
   308  		cmd.dot.r.p2 = cmd.b.nc
   309  		cmd.dot.r.p1 = cmd.dot.r.p2
   310  		telldot(cmd)
   311  	}
   312  }
   313  
   314  func delete(f *File) {
   315  	if downloaded && f.rasp != nil {
   316  		outTs(Hclose, f.tag)
   317  	}
   318  	delfile(f)
   319  	if f == curfile {
   320  		current(nil)
   321  	}
   322  }
   323  
   324  func update() {
   325  	settempfile()
   326  	anymod := 0
   327  	for _, f := range tempfile {
   328  		if f == cmd { /* cmd gets done in main() */
   329  			continue
   330  		}
   331  		if f.deleted {
   332  			delete(f)
   333  			continue
   334  		}
   335  		if f.seq == seq && fileupdate(f, false, downloaded) {
   336  			anymod++
   337  		}
   338  		if f.rasp != nil {
   339  			telldot(f)
   340  		}
   341  	}
   342  	if anymod != 0 {
   343  		seq++
   344  	}
   345  }
   346  
   347  func current(f *File) *File {
   348  	curfile = f
   349  	return curfile
   350  }
   351  
   352  func edit(f *File, cmd rune) {
   353  	empty := true
   354  	if cmd == 'r' {
   355  		logdelete(f, addr.r.p1, addr.r.p2)
   356  	}
   357  	if cmd == 'e' || cmd == 'I' {
   358  		logdelete(f, Posn(0), f.b.nc)
   359  		addr.r.p2 = f.b.nc
   360  	} else if f.b.nc != 0 || (f.name.s != nil && Strcmp(&genstr, &f.name) != 0) {
   361  		empty = false
   362  	}
   363  	var err error
   364  	iofile, err = os.Open(genc)
   365  	if err != nil {
   366  		if curfile != nil && curfile.unread {
   367  			curfile.unread = false
   368  		}
   369  		error_r(Eopen, genc, err)
   370  	}
   371  	var nulls bool
   372  	p := readio(f, &nulls, empty, true)
   373  	cp := p
   374  	if cmd == 'e' || cmd == 'I' {
   375  		cp = -1
   376  	}
   377  	closeio(cp)
   378  	if cmd == 'r' {
   379  		f.ndot.r.p1 = addr.r.p2
   380  		f.ndot.r.p2 = addr.r.p2 + p
   381  	} else {
   382  		f.ndot.r.p2 = 0
   383  		f.ndot.r.p1 = f.ndot.r.p2
   384  	}
   385  	f.closeok = empty
   386  	if quitok {
   387  		quitok = empty
   388  	} else {
   389  		quitok = false
   390  	}
   391  	m := Clean
   392  	if !empty && nulls {
   393  		m = Dirty
   394  	}
   395  	state(f, m)
   396  	if empty && !nulls {
   397  		f.cleanseq = f.seq
   398  	}
   399  	if cmd == 'e' {
   400  		filename(f)
   401  	}
   402  }
   403  
   404  func getname(f *File, s *String, save bool) int {
   405  	Strzero(&genstr)
   406  	genc = ""
   407  	var c rune
   408  	if s == nil || len(s.s) == 0 { /* no name provided */
   409  		if f != nil {
   410  			Strduplstr(&genstr, &f.name)
   411  		}
   412  	} else {
   413  		c = s.s[0]
   414  		if c != ' ' && c != '\t' {
   415  			error_(Eblank)
   416  		}
   417  		var i int
   418  		for i = 0; i < len(s.s); i++ {
   419  			c = s.s[i]
   420  			if !(c == ' ') && !(c == '\t') {
   421  				break
   422  			}
   423  		}
   424  		for i < len(s.s) && s.s[i] > ' ' {
   425  			Straddc(&genstr, s.s[i])
   426  			i++
   427  		}
   428  		if i != len(s.s) {
   429  			error_(Enewline)
   430  		}
   431  		fixname(&genstr)
   432  		if f != nil && (save || len(f.name.s) == 0) {
   433  			logsetname(f, &genstr)
   434  			if Strcmp(&f.name, &genstr) != 0 {
   435  				f.closeok = false
   436  				quitok = f.closeok
   437  				f.info = nil
   438  				state(f, Dirty) /* if it's 'e', fix later */
   439  			}
   440  		}
   441  	}
   442  	genc = Strtoc(&genstr)
   443  	return len(genstr.s)
   444  }
   445  
   446  func filename(f *File) {
   447  	genc = string(genstr.s)
   448  	ch := func(s string, b bool) byte {
   449  		if b {
   450  			return s[1]
   451  		}
   452  		return s[0]
   453  	}
   454  	dprint("%c%c%c %s\n", ch(" '", f.mod), ch("-+", f.rasp != nil), ch(" .", f == curfile), genc)
   455  }
   456  
   457  func undostep(f *File, isundo bool) {
   458  	mod := f.mod
   459  	var p1 int
   460  	var p2 int
   461  	fileundo(f, isundo, true, &p1, &p2, true)
   462  	f.ndot = f.dot
   463  	if f.mod {
   464  		f.closeok = false
   465  		quitok = false
   466  	} else {
   467  		f.closeok = true
   468  	}
   469  
   470  	if f.mod != mod {
   471  		f.mod = mod
   472  		m := Clean
   473  		if mod {
   474  			m = Dirty
   475  		}
   476  		state(f, m)
   477  	}
   478  }
   479  
   480  func undo(isundo bool) int {
   481  	max := undoseq(curfile, isundo)
   482  	if max == 0 {
   483  		return 0
   484  	}
   485  	settempfile()
   486  	for _, f := range tempfile {
   487  		if f != cmd && undoseq(f, isundo) == max {
   488  			undostep(f, isundo)
   489  		}
   490  	}
   491  	return 1
   492  }
   493  
   494  func readcmd(s *String) int {
   495  	if flist != nil {
   496  		fileclose(flist)
   497  	}
   498  	flist = fileopen()
   499  
   500  	addr.r.p1 = 0
   501  	addr.r.p2 = flist.b.nc
   502  	retcode := plan9(flist, '<', s, false)
   503  	fileupdate(flist, false, false)
   504  	flist.seq = 0
   505  	if flist.b.nc > BLOCKSIZE {
   506  		error_(Etoolong)
   507  	}
   508  	Strzero(&genstr)
   509  	Strinsure(&genstr, flist.b.nc)
   510  	bufread(&flist.b, Posn(0), genbuf[:flist.b.nc])
   511  	copy(genstr.s, genbuf[:])
   512  	return retcode
   513  }
   514  
   515  func getcurwd() {
   516  	wd, _ := os.Getwd()
   517  	t := tmpcstr(wd)
   518  	Strduplstr(&curwd, t)
   519  	freetmpstr(t)
   520  	if len(curwd.s) == 0 {
   521  		warn(Wpwd)
   522  	} else if curwd.s[len(curwd.s)-1] != '/' {
   523  		Straddc(&curwd, '/')
   524  	}
   525  }
   526  
   527  func cd(str *String) {
   528  	getcurwd()
   529  	var s string
   530  	if getname(nil, str, false) != 0 {
   531  		s = genc
   532  	} else {
   533  		s = home
   534  	}
   535  	if err := os.Chdir(s); err != nil {
   536  		syserror("chdir", err)
   537  	}
   538  	/*
   539  		fd := syscall.Open("/dev/wdir", syscall.O_WRONLY)
   540  		if fd > 0 {
   541  			write(fd, s, strlen(s))
   542  		}
   543  	*/
   544  	dprint("!\n")
   545  	var owd String
   546  	Strinit(&owd)
   547  	Strduplstr(&owd, &curwd)
   548  	getcurwd()
   549  	settempfile()
   550  	/*
   551  	 * Two passes so that if we have open
   552  	 * /a/foo.c and /b/foo.c and cd from /b to /a,
   553  	 * we don't ever have two foo.c simultaneously.
   554  	 */
   555  	for _, f := range tempfile {
   556  		if f != cmd && len(f.name.s) > 0 && f.name.s[0] != '/' {
   557  			Strinsert(&f.name, &owd, Posn(0))
   558  			fixname(&f.name)
   559  			sortname(f)
   560  		}
   561  	}
   562  	for _, f := range tempfile {
   563  		if f != cmd && Strispre(&curwd, &f.name) {
   564  			fixname(&f.name)
   565  			sortname(f)
   566  		}
   567  	}
   568  	Strclose(&owd)
   569  }
   570  
   571  func loadflist(s *String) bool {
   572  	var i int
   573  	var c rune
   574  	if len(s.s) > 0 {
   575  		c = s.s[0]
   576  	}
   577  	for i = 0; i < len(s.s) && (s.s[i] == ' ' || s.s[i] == '\t'); i++ {
   578  	}
   579  	if (c == ' ' || c == '\t') && (i >= len(s.s) || s.s[i] != '\n') {
   580  		if i < len(s.s) && s.s[i] == '<' {
   581  			Strdelete(s, 0, int(i)+1)
   582  			readcmd(s)
   583  		} else {
   584  			Strzero(&genstr)
   585  			for ; i < len(s.s); i++ {
   586  				c := s.s[i]
   587  				if c == '\n' {
   588  					break
   589  				}
   590  				Straddc(&genstr, c)
   591  			}
   592  		}
   593  	} else {
   594  		if c != '\n' {
   595  			error_(Eblank)
   596  		}
   597  		Strdupl(&genstr, empty)
   598  	}
   599  	genc = Strtoc(&genstr)
   600  	debug("loadflist %s\n", genc)
   601  	return len(genstr.s) > 0
   602  }
   603  
   604  func readflist(readall, delete bool) *File {
   605  	var t String
   606  	Strinit(&t)
   607  	i := 0
   608  	var f *File
   609  	for ; f == nil || readall || delete; i++ { /* ++ skips blank */
   610  		debug("readflist %q\n", string(genstr.s))
   611  		Strdelete(&genstr, Posn(0), i)
   612  		for i = 0; i < len(genstr.s); i++ {
   613  			c := genstr.s[i]
   614  			if c != ' ' && c != '\t' && c != '\n' {
   615  				break
   616  			}
   617  		}
   618  		if i >= len(genstr.s) {
   619  			break
   620  		}
   621  		Strdelete(&genstr, Posn(0), i)
   622  		for i = 0; i < len(genstr.s); i++ {
   623  			c := genstr.s[i]
   624  			if c == ' ' || c == '\t' || c == '\n' {
   625  				break
   626  			}
   627  		}
   628  		if i == 0 {
   629  			break
   630  		}
   631  		Strduplstr(&t, tmprstr(genstr.s[:i]))
   632  		debug("dup %s\n", string(t.s))
   633  		fixname(&t)
   634  		debug("lookfile %s\n", string(t.s))
   635  		f = lookfile(&t)
   636  		if delete {
   637  			if f == nil {
   638  				warn_S(Wfile, &t)
   639  			} else {
   640  				trytoclose(f)
   641  			}
   642  		} else if f == nil && readall {
   643  			f = newfile()
   644  			logsetname(f, &t)
   645  		}
   646  	}
   647  	Strclose(&t)
   648  	return f
   649  }
   650  
   651  func tofile(s *String) *File {
   652  	if s.s[0] != ' ' {
   653  		error_(Eblank)
   654  	}
   655  	var f *File
   656  	if !loadflist(s) {
   657  		f = lookfile(&genstr) /* empty string ==> nameless file */
   658  		if f == nil {
   659  			error_s(Emenu, genc)
   660  		}
   661  	} else {
   662  		f = readflist(false, false)
   663  		if f == nil {
   664  			error_s(Emenu, genc)
   665  		}
   666  	}
   667  	return current(f)
   668  }
   669  
   670  func getfile(s *String) *File {
   671  	var f *File
   672  	if !loadflist(s) {
   673  		f = newfile()
   674  		logsetname(f, &genstr)
   675  	} else {
   676  		debug("read? %q\n", genc)
   677  		f = readflist(true, false)
   678  		if f == nil {
   679  			error_(Eblank)
   680  		}
   681  	}
   682  	return current(f)
   683  }
   684  
   685  func closefiles(f *File, s *String) {
   686  	if len(s.s) == 0 {
   687  		if f == nil {
   688  			error_(Enofile)
   689  		}
   690  		trytoclose(f)
   691  		return
   692  	}
   693  	if s.s[0] != ' ' {
   694  		error_(Eblank)
   695  	}
   696  	if !loadflist(s) {
   697  		error_(Enewline)
   698  	}
   699  	readflist(false, true)
   700  }
   701  
   702  func fcopy(f *File, addr2 Address) {
   703  	var ni int
   704  	for p := addr.r.p1; p < addr.r.p2; p += ni {
   705  		ni = addr.r.p2 - p
   706  		if ni > BLOCKSIZE {
   707  			ni = BLOCKSIZE
   708  		}
   709  		bufread(&f.b, p, genbuf[:ni])
   710  		loginsert(addr2.f, addr2.r.p2, tmprstr(genbuf[:ni]).s)
   711  	}
   712  	addr2.f.ndot.r.p2 = addr2.r.p2 + (f.dot.r.p2 - f.dot.r.p1)
   713  	addr2.f.ndot.r.p1 = addr2.r.p2
   714  }
   715  
   716  func move(f *File, addr2 Address) {
   717  	if addr.r.p2 <= addr2.r.p2 {
   718  		logdelete(f, addr.r.p1, addr.r.p2)
   719  		fcopy(f, addr2)
   720  	} else if addr.r.p1 >= addr2.r.p2 {
   721  		fcopy(f, addr2)
   722  		logdelete(f, addr.r.p1, addr.r.p2)
   723  	} else {
   724  		error_(Eoverlap)
   725  	}
   726  }
   727  
   728  func nlcount(f *File, p0 Posn, p1 Posn) Posn {
   729  	nl := 0
   730  
   731  	for p0 < p1 {
   732  		tmp30 := p0
   733  		p0++
   734  		if filereadc(f, tmp30) == '\n' {
   735  			nl++
   736  		}
   737  	}
   738  	return nl
   739  }
   740  
   741  func printposn(f *File, charsonly bool) {
   742  	if !charsonly {
   743  		l1 := 1 + nlcount(f, Posn(0), addr.r.p1)
   744  		l2 := l1 + nlcount(f, addr.r.p1, addr.r.p2)
   745  		/* check if addr ends with '\n' */
   746  		if addr.r.p2 > 0 && addr.r.p2 > addr.r.p1 && filereadc(f, addr.r.p2-1) == '\n' {
   747  			l2--
   748  		}
   749  		dprint("%d", l1)
   750  		if l2 != l1 {
   751  			dprint(",%d", l2)
   752  		}
   753  		dprint("; ")
   754  	}
   755  	dprint("#%d", addr.r.p1)
   756  	if addr.r.p2 != addr.r.p1 {
   757  		dprint(",#%d", addr.r.p2)
   758  	}
   759  	dprint("\n")
   760  }
   761  
   762  func settempfile() {
   763  	tempfile = append(tempfile[:0], file...)
   764  }