├── Makefile ├── README.md ├── go.mod ├── go.sum ├── goatee.conf ├── goatee.desktop ├── goatee.go ├── goatee_conf.go ├── goatee_tabs.go ├── goatee_ui.go ├── hex.lang └── screenshots ├── binary.png └── text.png /Makefile: -------------------------------------------------------------------------------- 1 | # goatee 2 | 3 | 4 | run: build 5 | ./goatee ${ARGS} 6 | 7 | get: 8 | go get ./... 9 | 10 | build: 11 | go build -ldflags="-s -w" -gcflags="-trimpath=${GOPATH}/src" -asmflags="-trimpath=${GOPATH}/src" 12 | 13 | goinstall: 14 | go install 15 | 16 | install: 17 | cp ./goatee ${DESTDIR} 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GOATee - simple gtk2 text editor written on Go 2 | 3 | # Configure 4 | 5 | `goatee.conf` is example of config file, text editor tries to get it by `XDG_CONFIG_PATH/goatee/` or from working directory. 6 | 7 | # Features 8 | 9 | * multiple homogeneous(*full width*) Tabs 10 | * auto detect charset and binary files 11 | * smart detect language(syntax) for text files 12 | * hex editor for binary files with search and replace 13 | 14 | # Screenshots 15 | 16 | 17 | **text file:** 18 | 19 | ![text](screenshots/text.png) 20 | 21 | 22 | **binary file with hidden menu:** 23 | 24 | ![binary](screenshots/binary.png) 25 | 26 | 27 | 28 | # Requirements 29 | 30 | * gtk2 31 | * gtksourceview2 32 | 33 | 34 | # Knownbugs 35 | * for hex view regexp replace not work 36 | * for hex view search with regexp, some expressions not correct, because search is performed for a hex string not for a byte array -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sg3des/goatee 2 | 3 | go 1.21.5 4 | 5 | require ( 6 | github.com/djimenez/iconv-go v0.0.0-20160305225143-8960e66bd3da 7 | github.com/mattn/go-gtk v0.0.0-20240119050609-48574e312fac 8 | github.com/naoina/toml v0.1.1 9 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d 10 | ) 11 | 12 | require ( 13 | github.com/kylelemons/godebug v1.1.0 // indirect 14 | github.com/mattn/go-pointer v0.0.1 // indirect 15 | github.com/naoina/go-stringutil v0.1.0 // indirect 16 | github.com/stretchr/testify v1.8.4 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/djimenez/iconv-go v0.0.0-20160305225143-8960e66bd3da h1:0qwwqQCLOOXPl58ljnq3sTJR7yRuMolM02vjxDh4ZVE= 4 | github.com/djimenez/iconv-go v0.0.0-20160305225143-8960e66bd3da/go.mod h1:ns+zIWBBchgfRdxNgIJWn2x6U95LQchxeqiN5Cgdgts= 5 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 6 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 7 | github.com/mattn/go-gtk v0.0.0-20230419095350-e099c1bf7abc h1:L1NPQxvNA/Ad3evWGMX8lJPSmPo+b4L9Y5lsi0w1orA= 8 | github.com/mattn/go-gtk v0.0.0-20230419095350-e099c1bf7abc/go.mod h1:PwzwfeB5syFHXORC3MtPylVcjIoTDT/9cvkKpEndGVI= 9 | github.com/mattn/go-gtk v0.0.0-20240119050609-48574e312fac h1:tNm7zRceQAOg9D8vQFq0K9hy49j39+9+7rSjML4YREI= 10 | github.com/mattn/go-gtk v0.0.0-20240119050609-48574e312fac/go.mod h1:PwzwfeB5syFHXORC3MtPylVcjIoTDT/9cvkKpEndGVI= 11 | github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= 12 | github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= 13 | github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hzifhks= 14 | github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0= 15 | github.com/naoina/toml v0.1.1 h1:PT/lllxVVN0gzzSqSlHEmP8MJB4MY2U7STGxiouV4X8= 16 | github.com/naoina/toml v0.1.1/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= 20 | github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= 21 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 22 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 24 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | -------------------------------------------------------------------------------- /goatee.conf: -------------------------------------------------------------------------------- 1 | [ui] 2 | menubar-visible = false 3 | statusbar-visible = false 4 | 5 | [text_view] 6 | font = "Liberation Mono 8" 7 | line-hightlight = true 8 | line-numbers = true 9 | word-wrap = true 10 | indent-space = false 11 | indent-width = 2 12 | style-scheme = "classic" 13 | 14 | [tabs] 15 | homogeneous = false 16 | close-buttons = false 17 | height = 12 18 | fg-normal = [200, 200, 200] 19 | fg-modified = [220, 20, 20] 20 | fg-new = [250, 200, 10] 21 | 22 | [search] 23 | max-items = 1024 24 | 25 | [hex] 26 | bytes-in-line = 16 27 | -------------------------------------------------------------------------------- /goatee.desktop: -------------------------------------------------------------------------------- 1 | 2 | [Desktop Entry] 3 | Name=GOATee 4 | Comment=Simple Text Editor 5 | Comment[ar]=محرر نصوص بسيط 6 | Comment[ast]=Editor de testu simple 7 | Comment[bg]=Опростен текстов редактор 8 | Comment[cs]=Jednoduchý textový editor 9 | Comment[de]=Einfache Textbearbeitung 10 | Comment[el]=Απλός επεξεργαστής κειμένου 11 | Comment[en_AU]=Simple Text Editor 12 | Comment[en_GB]=Simple Text Editor 13 | Comment[es]=Un simple editor de texto 14 | Comment[fi]=Yksinkertainen tekstimuokkain 15 | Comment[fr]=Éditeur de texte simple 16 | Comment[hr]=Jednostavni uređivač teksta 17 | Comment[hu]=Egyszerű szövegszerkesztő 18 | Comment[id]=Penyunting Teks Sederhana 19 | Comment[is]=Einfaldur textaritill 20 | Comment[it]=Semplice editor di testo 21 | Comment[ja]=シンプルなテキストエディターです 22 | Comment[kk]=Қарапайым мәтін түзетушісі 23 | Comment[ko]=간단한 문서 편집기 24 | Comment[lt]=Paprastas teksto redaktorius 25 | Comment[ms]=Penyunting Teks Ringkas 26 | Comment[nb]=Enkel tekstbehandler 27 | Comment[nl]=Eenvoudige tekstbewerker 28 | Comment[oc]=Editor de tèxte simple 29 | Comment[pl]=Zwykły edytor tekstu 30 | Comment[pt]=Editor de texto simples 31 | Comment[pt_BR]=Editor de Texto Simples 32 | Comment[ro]=Un editor simplu de text 33 | Comment[ru]=Простой текстовый редактор 34 | Comment[sk]=Jednoduchý textový editor 35 | Comment[sr]=Једноставан уређивач текста 36 | Comment[sv]=Enkel textredigerare 37 | Comment[te]=సులభ పాఠ్య కూర్పకం 38 | Comment[th]=เครื่องมือแก้ไขข้อความอย่างง่าย 39 | Comment[tr]=Basit Metin Düzenleyici 40 | Comment[ug]=ئاددىي تېكىست تەھرىرلىگۈ 41 | Comment[uk]=Простий текстовий редактор 42 | Comment[zh_CN]=简易文本编辑器 43 | Comment[zh_TW]=簡易文字編輯程式 44 | GenericName=Text Editor 45 | GenericName[ar]=محرر نصوص 46 | GenericName[ast]=Editor de testu 47 | GenericName[bg]=Текстов редактор 48 | GenericName[cs]=Textový editor 49 | GenericName[de]=Textbearbeitung 50 | GenericName[el]=Επεξεργαστής Κειμένου 51 | GenericName[en_AU]=Text Editor 52 | GenericName[en_GB]=Text Editor 53 | GenericName[es]=Editor de texto 54 | GenericName[fi]=Tekstimuokkain 55 | GenericName[fr]=Éditeur de texte 56 | GenericName[hr]=Uređivač teksta 57 | GenericName[hu]=Szövegszerkesztő 58 | GenericName[id]=Penyunting Teks 59 | GenericName[is]=Textaritill 60 | GenericName[it]=Editor di Testo 61 | GenericName[ja]=テキストエディター 62 | GenericName[kk]=Мәтін түзетушісі 63 | GenericName[ko]=문서 편집기 64 | GenericName[lt]=Teksto redaktorius 65 | GenericName[ms]=Penyunting Teks 66 | GenericName[nb]=Tekstbehandler 67 | GenericName[nl]=Tekstbewerker 68 | GenericName[oc]=Editor de tèxte 69 | GenericName[pl]=Edytor tekstu 70 | GenericName[pt]=Editor de texto 71 | GenericName[pt_BR]=Editor de Texto 72 | GenericName[ro]=Editor de text 73 | GenericName[ru]=Текстовый редактор 74 | GenericName[sk]=Textový editor 75 | GenericName[sr]=Уређивач текста 76 | GenericName[sv]=Textredigerare 77 | GenericName[te]=పాఠ్య కూర్పకం 78 | GenericName[th]=เครื่องมือแก้ไขข้อความ 79 | GenericName[tr]=Metin Düzenleyici 80 | GenericName[ug]=تېكىست تەھرىرلىگۈ 81 | GenericName[uk]=Текстовий редактор 82 | GenericName[zh_CN]=文本编辑器 83 | GenericName[zh_TW]=文字編輯程式 84 | Exec=goatee %F 85 | Icon=accessories-text-editor 86 | Terminal=false 87 | StartupNotify=true 88 | Type=Application 89 | Categories=TextEditor;GTK; 90 | MimeType=text/plain; 91 | -------------------------------------------------------------------------------- /goatee.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "net/url" 7 | "os" 8 | "os/user" 9 | "strings" 10 | 11 | "github.com/mattn/go-gtk/gdk" 12 | "github.com/mattn/go-gtk/gio" 13 | "github.com/mattn/go-gtk/gtk" 14 | gsv "github.com/mattn/go-gtk/gtksourceview" 15 | ) 16 | 17 | var ( 18 | ui *UI 19 | conf *Conf 20 | 21 | gvfsPath = "/run/user/%s/gvfs/" 22 | 23 | newtabiter int 24 | 25 | langManager = gsv.SourceLanguageManagerGetDefault() 26 | languages = langManager.GetLanguageIds() 27 | 28 | charsets = []string{ 29 | CHARSET_UTF8, 30 | "utf-16", 31 | "", 32 | "ISO-8859-2", 33 | "ISO-8859-7", 34 | "ISO-8859-9", 35 | "ISO-8859-15", 36 | "ShiftJIS", 37 | "EUC-KR", 38 | "gb18030", 39 | "Big5", 40 | "TIS-620", 41 | "KOI8-R", 42 | "", 43 | "windows-874", 44 | "windows-1250", 45 | "windows-1251", 46 | "windows-1252", 47 | "windows-1253", 48 | "windows-1254", 49 | "windows-1255", 50 | "windows-1256", 51 | "windows-1257", 52 | "windows-1258", 53 | "", 54 | CHARSET_BINARY} 55 | ) 56 | 57 | func init() { 58 | // log.SetFlags(log.Lshortfile) 59 | 60 | user, _ := user.Current() 61 | gvfsPath = fmt.Sprintf(gvfsPath, user.Uid) 62 | 63 | conf = NewConf() 64 | } 65 | 66 | func main() { 67 | gtk.Init(nil) 68 | ui = CreateUI() 69 | 70 | switch { 71 | case len(os.Args) == 1: 72 | ui.NewTab("") 73 | case os.Args[1] == "--help" || os.Args[1] == "-h": 74 | fmt.Println("Usage:\n\tgoatee [files...]") 75 | os.Exit(0) 76 | default: 77 | for i := 1; i < len(os.Args); i++ { 78 | ui.NewTab(os.Args[i]) 79 | } 80 | } 81 | 82 | gtk.Main() 83 | } 84 | 85 | func issetLanguage(lang string) bool { 86 | for _, langID := range languages { 87 | if langID == lang { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | 94 | func xmlLanguages() string { 95 | //construct sections 96 | structure := structureLanguages() 97 | 98 | var xmldata []string 99 | for section, langs := range structure { 100 | xmldata = append(xmldata, "") 101 | for _, l := range langs { 102 | xmldata = append(xmldata, "") 103 | } 104 | xmldata = append(xmldata, "") 105 | } 106 | 107 | return strings.Join(xmldata, "\n") 108 | } 109 | 110 | type language struct { 111 | n int 112 | name string 113 | } 114 | 115 | func structureLanguages() map[string][]language { 116 | var structure = make(map[string][]language) 117 | for n, langname := range languages { 118 | lang := langManager.GetLanguage(langname) 119 | section := lang.GetSection() 120 | if _, ok := structure[section]; !ok { 121 | structure[section] = []language{} 122 | } 123 | structure[section] = append(structure[section], language{n, langname}) 124 | } 125 | return structure 126 | } 127 | 128 | func xmlEncodings() string { 129 | var xmldata []string 130 | for _, c := range charsets { 131 | if len(c) == 0 { 132 | xmldata = append(xmldata, "") 133 | } else { 134 | xmldata = append(xmldata, "") 135 | } 136 | } 137 | return strings.Join(xmldata, "\n") 138 | } 139 | 140 | func errorMessage(err error) { 141 | m := gtk.NewMessageDialogWithMarkup(nil, gtk.DIALOG_MODAL, gtk.MESSAGE_ERROR, gtk.BUTTONS_CLOSE, err.Error()) 142 | m.Run() 143 | m.Destroy() 144 | } 145 | 146 | func convertColor(col []int) *gdk.Color { 147 | r := uint16(math.Pow(float64(col[0]), 2)) 148 | g := uint16(math.Pow(float64(col[1]), 2)) 149 | b := uint16(math.Pow(float64(col[2]), 2)) 150 | 151 | return gdk.NewColorRGB(r, g, b) 152 | } 153 | 154 | func resolveFilename(filename string) string { 155 | u, err := url.Parse(filename) 156 | if err != nil { 157 | return filename 158 | } 159 | 160 | //if scheme exist, ex: 'sftp://' then parse path with how URI 161 | if u.Scheme != "" { 162 | filename = gio.NewGFileForURI(filename).GetPath() 163 | } else { 164 | filename = gio.NewGFileForPath(filename).GetPath() 165 | } 166 | 167 | return filename 168 | } 169 | -------------------------------------------------------------------------------- /goatee_conf.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "math" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | "reflect" 12 | "regexp" 13 | "strconv" 14 | "strings" 15 | 16 | "github.com/mattn/go-gtk/gdk" 17 | "github.com/mattn/go-gtk/gtk" 18 | gsv "github.com/mattn/go-gtk/gtksourceview" 19 | 20 | "github.com/naoina/toml" 21 | ) 22 | 23 | //Conf structure contains configuration 24 | type Conf struct { 25 | window *gtk.Window `toml:",omitempty"` 26 | filename string `toml:",omitempty"` 27 | schemeManager *gsv.SourceStyleSchemeManager `toml:",omitempty"` 28 | 29 | UI struct { 30 | MenuBarVisible bool `toml:"menubar-visible" wgt:"checkbox"` 31 | StatusBarVisible bool `toml:"statusbar-visible" wgt:"checkbox"` 32 | } 33 | TextView struct { 34 | Font string `toml:"font" wgt:"font"` 35 | LineHightlight bool `toml:"line-hightlight" wgt:"checkbox"` 36 | LineNumbers bool `toml:"line-numbers" wgt:"checkbox"` 37 | WordWrap bool `toml:"word-wrap" wgt:"checkbox"` 38 | IndentSpace bool `toml:"indent-space" wgt:"checkbox"` 39 | IndentWidth int `toml:"indent-width" wgt:"int"` 40 | StyleScheme string `toml:"style-scheme" wgt:"schemes"` 41 | } 42 | Tabs struct { 43 | Homogeneous bool `toml:"homogeneous" wgt:"checkbox"` 44 | CloseBtns bool `toml:"close-buttons" wgt:"checkbox"` 45 | Height int `toml:"height" wgt:"int"` 46 | FGNormal []int `toml:"fg-normal" wgt:"color"` 47 | FGModified []int `toml:"fg-modified" wgt:"color"` 48 | FGNew []int `toml:"fg-new" wgt:"color"` 49 | } 50 | Search struct { 51 | MaxItems int `toml:"max-items" wgt:"int"` 52 | } 53 | Hex struct { 54 | BytesInLine int `toml:"bytes-in-line" wgt:"int"` 55 | } 56 | } 57 | 58 | //NewConf set default values for configuration and parse config file 59 | func NewConf() *Conf { 60 | confdir := os.Getenv("XDG_CONFIG_HOME") 61 | if confdir == "" { 62 | confdir = path.Join(os.Getenv("HOME"), ".config") 63 | } 64 | confdir = path.Join(confdir, "goatee") 65 | 66 | configfiles := []string{ 67 | path.Join(confdir, "goatee.conf"), 68 | "goatee.conf", 69 | } 70 | 71 | // default values 72 | c := new(Conf) 73 | c.filename = path.Join(confdir, "goatee.conf") 74 | c.schemeManager = gsv.SourceStyleSchemeManagerGetDefault() 75 | 76 | c.UI.MenuBarVisible = true 77 | c.UI.StatusBarVisible = false 78 | 79 | c.TextView.Font = "Liberation Mono 8" 80 | c.TextView.LineHightlight = true 81 | c.TextView.LineNumbers = true 82 | c.TextView.WordWrap = true 83 | c.TextView.IndentSpace = false 84 | c.TextView.IndentWidth = 2 85 | 86 | c.Tabs.Homogeneous = true 87 | c.Tabs.CloseBtns = true 88 | c.Tabs.Height = 16 89 | c.Tabs.FGNormal = []int{200, 200, 200} 90 | c.Tabs.FGModified = []int{220, 20, 20} 91 | c.Tabs.FGNew = []int{250, 200, 10} 92 | 93 | c.Search.MaxItems = 1024 94 | 95 | c.Hex.BytesInLine = 16 96 | 97 | //parse config files 98 | for _, filename := range configfiles { 99 | if err := c.readConfigFile(filename); err == nil { 100 | c.filename = filename 101 | break 102 | } 103 | } 104 | 105 | return c 106 | } 107 | 108 | func (c *Conf) readConfigFile(filename string) error { 109 | data, err := ioutil.ReadFile(filename) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | if err = toml.Unmarshal(data, &c); err != nil { 115 | err = fmt.Errorf("failed decode config file '%s', reason: %s", filename, err) 116 | log.Println(err) 117 | return err 118 | } 119 | 120 | return nil 121 | } 122 | 123 | func (c *Conf) Write() { 124 | os.MkdirAll(filepath.Dir(c.filename), 0755) 125 | 126 | if c.filename == "" { 127 | return 128 | } 129 | 130 | data, err := toml.Marshal(&c) 131 | if err != nil { 132 | log.Println(err) 133 | return 134 | } 135 | 136 | err = ioutil.WriteFile(c.filename, data, 0644) 137 | if err != nil { 138 | log.Println(err) 139 | return 140 | } 141 | } 142 | 143 | //OpenWindow open window configuration 144 | func (c *Conf) OpenWindow() { 145 | if c.window == nil { 146 | c.CreateWindow() 147 | } 148 | c.window.ShowAll() 149 | } 150 | 151 | //CreateWindow create window configuration 152 | func (c *Conf) CreateWindow() { 153 | c.window = gtk.NewWindow(gtk.WINDOW_TOPLEVEL) 154 | c.window.SetName("Preferences") 155 | c.window.SetTypeHint(gdk.WINDOW_TYPE_HINT_DIALOG) 156 | c.window.SetDefaultSize(300, 300) 157 | c.window.SetSizeRequest(300, 300) 158 | 159 | vbox := gtk.NewVBox(false, 0) 160 | 161 | notebook := gtk.NewNotebook() 162 | 163 | c.readConfigFile(c.filename) 164 | 165 | t := reflect.TypeOf(c).Elem() 166 | v := reflect.ValueOf(c).Elem() 167 | for i := 0; i < t.NumField(); i++ { 168 | if t.Field(i).Type.Kind() != reflect.Struct { 169 | continue 170 | } 171 | 172 | fvbox := gtk.NewVBox(false, 0) 173 | notebook.AppendPage(fvbox, gtk.NewLabel(t.Field(i).Name)) 174 | 175 | confStruct := t.Field(i).Type 176 | for j := 0; j < confStruct.NumField(); j++ { 177 | field := confStruct.Field(j) 178 | val := v.Field(i).Field(j) 179 | 180 | label, widget := c.newWidget(val, field) 181 | 182 | hbox := gtk.NewHBox(false, 0) 183 | hbox.PackStart(label, false, false, 5) 184 | hbox.PackEnd(widget, false, false, 5) 185 | 186 | fvbox.PackStart(hbox, false, false, 5) 187 | } 188 | } 189 | 190 | closebtn := gtk.NewButtonFromStock(gtk.STOCK_CLOSE) 191 | closebtn.Clicked(c.CloseWindow) 192 | hbox := gtk.NewHBox(false, 0) 193 | hbox.PackEnd(closebtn, false, false, 5) 194 | 195 | vbox.Add(notebook) 196 | vbox.PackEnd(hbox, false, false, 5) 197 | 198 | c.window.Add(vbox) 199 | } 200 | 201 | func (c *Conf) newWidget(v reflect.Value, f reflect.StructField) (*gtk.Label, gtk.IWidget) { 202 | name := c.getFieldName(f) 203 | label := gtk.NewLabel(name) 204 | 205 | tag, ok := f.Tag.Lookup("wgt") 206 | if !ok { 207 | log.Fatalf("tag `wgt` not set for field %s", name) 208 | } 209 | 210 | w := &ConfWidget{Field: v, conf: c} 211 | 212 | switch tag { 213 | case "checkbox": 214 | w.chkbtn = gtk.NewCheckButton() 215 | w.chkbtn.SetSizeRequest(150, -1) 216 | w.chkbtn.SetActive(v.Bool()) 217 | w.chkbtn.Connect("clicked", w.UpdateValue) 218 | 219 | case "string": 220 | w.entry = gtk.NewEntry() 221 | w.entry.SetSizeRequest(150, -1) 222 | w.entry.SetText(v.String()) 223 | w.entry.Connect("changed", w.UpdateValue) 224 | 225 | case "int": 226 | w.spnbtn = gtk.NewSpinButtonWithRange(-1, 2048, 1) 227 | w.spnbtn.SetSizeRequest(150, -1) 228 | w.spnbtn.SetValue(float64(v.Int())) 229 | w.spnbtn.Connect("changed", w.UpdateValue) 230 | 231 | case "color": 232 | color := convertColor(v.Interface().([]int)) 233 | w.colbtn = gtk.NewColorButtonWithColor(color) 234 | w.colbtn.SetSizeRequest(150, -1) 235 | w.colbtn.Connect("color-set", w.UpdateValue) 236 | 237 | case "font": 238 | w.fntbtn = gtk.NewFontButton() 239 | w.fntbtn.SetSizeRequest(150, -1) 240 | w.fntbtn.SetFontName(v.String()) 241 | w.fntbtn.Connect("font-set", w.UpdateValue) 242 | 243 | case "schemes": 244 | schemes := c.schemeManager.GetSchemeIds() 245 | scheme := v.String() 246 | 247 | w.cmbbox = gtk.NewComboBoxText() 248 | w.cmbbox.SetSizeRequest(150, -1) 249 | for i, s := range schemes { 250 | w.cmbbox.AppendText(s) 251 | if scheme == s { 252 | w.cmbbox.SetActive(i) 253 | } 254 | } 255 | w.cmbbox.Connect("changed", w.UpdateValue) 256 | } 257 | 258 | return label, w.GetWidget() 259 | } 260 | 261 | func (c *Conf) getFieldName(f reflect.StructField) string { 262 | name := strings.Split(f.Tag.Get("toml"), ",")[0] 263 | if len(name) == 0 { 264 | name = f.Name 265 | } 266 | return c.FormatName(name) 267 | } 268 | 269 | func (c *Conf) CloseWindow() { 270 | c.Write() 271 | 272 | c.window.Hide() 273 | } 274 | 275 | type ConfWidget struct { 276 | Field reflect.Value 277 | 278 | conf *Conf 279 | 280 | chkbtn *gtk.CheckButton 281 | entry *gtk.Entry 282 | spnbtn *gtk.SpinButton 283 | colbtn *gtk.ColorButton 284 | fntbtn *gtk.FontButton 285 | cmbbox *gtk.ComboBoxText 286 | } 287 | 288 | func (w *ConfWidget) UpdateValue() { 289 | switch { 290 | case w.chkbtn != nil: 291 | w.Field.SetBool(w.chkbtn.GetActive()) 292 | case w.entry != nil: 293 | w.Field.SetString(w.entry.GetText()) 294 | case w.spnbtn != nil: 295 | n, _ := strconv.Atoi(w.spnbtn.Entry.GetText()) 296 | w.Field.SetInt(int64(n)) 297 | case w.colbtn != nil: 298 | col := w.colbtn.GetColor() 299 | r := int(math.Sqrt(float64(col.Red()))) 300 | g := int(math.Sqrt(float64(col.Green()))) 301 | b := int(math.Sqrt(float64(col.Blue()))) 302 | 303 | w.Field.Set(reflect.ValueOf([]int{r, g, b})) 304 | case w.fntbtn != nil: 305 | w.Field.SetString(w.fntbtn.GetFontName()) 306 | case w.cmbbox != nil: 307 | w.Field.SetString(w.cmbbox.GetActiveText()) 308 | } 309 | 310 | ui.TabsUpdateConf() 311 | } 312 | 313 | func (w *ConfWidget) GetWidget() gtk.IWidget { 314 | switch { 315 | case w.chkbtn != nil: 316 | return w.chkbtn 317 | case w.entry != nil: 318 | return w.entry 319 | case w.spnbtn != nil: 320 | return w.spnbtn 321 | case w.colbtn != nil: 322 | return w.colbtn 323 | case w.fntbtn != nil: 324 | return w.fntbtn 325 | case w.cmbbox != nil: 326 | return w.cmbbox 327 | } 328 | log.Println(w) 329 | return nil 330 | } 331 | 332 | func (c *Conf) FormatName(name string) string { 333 | name = strings.Replace(name, "-", " ", -1) 334 | name = regexp.MustCompile("([A-Z])").ReplaceAllString(name, " $1") 335 | name = strings.TrimSpace(name) 336 | name = strings.Title(name) 337 | return name 338 | } 339 | -------------------------------------------------------------------------------- /goatee_tabs.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "log" 12 | "net/http" 13 | "os" 14 | "path" 15 | "regexp" 16 | "strings" 17 | "unicode/utf8" 18 | "unsafe" 19 | 20 | "github.com/mattn/go-gtk/gdk" 21 | "github.com/mattn/go-gtk/glib" 22 | "github.com/mattn/go-gtk/gtk" 23 | 24 | gsv "github.com/mattn/go-gtk/gtksourceview" 25 | 26 | iconv "github.com/djimenez/iconv-go" 27 | "github.com/saintfish/chardet" 28 | ) 29 | 30 | type Tab struct { 31 | Filename string 32 | File *os.File 33 | Encoding string 34 | Language string 35 | ReadOnly bool 36 | Dirty bool 37 | 38 | eventbox *gtk.EventBox 39 | tab *gtk.HBox 40 | label *gtk.Label 41 | closeBtn *gtk.Button 42 | 43 | swin *gtk.ScrolledWindow 44 | sourceview *gsv.SourceView 45 | sourcebuffer *gsv.SourceBuffer 46 | 47 | cursorPos gtk.TextIter 48 | 49 | find string 50 | findtext string 51 | findindex [][]int 52 | findindexCurrent int 53 | findoffset int 54 | findwrap bool 55 | tagfind *gtk.TextTag 56 | tagfindCurrent *gtk.TextTag 57 | } 58 | 59 | func NewTab(filename string) (t *Tab) { 60 | if len(filename) > 0 { 61 | filename = resolveFilename(filename) 62 | } 63 | 64 | if len(filename) > 0 { 65 | var ok bool 66 | var n int 67 | 68 | //reload if this file already open 69 | if t, n, ok = ui.LookupTab(filename); ok { 70 | ui.notebook.SetCurrentPage(n) 71 | 72 | text, err := t.ReadFile(filename) 73 | if err != nil { 74 | errorMessage(err) 75 | log.Println(err) 76 | return 77 | } 78 | t.sourcebuffer.SetText(text) 79 | t.Dirty = false 80 | t.SetTabFGColor(conf.Tabs.FGNormal) 81 | //TODO: reopen 82 | return nil 83 | } 84 | } 85 | 86 | t = &Tab{ 87 | Encoding: CHARSET_UTF8, 88 | Language: "sh", 89 | } 90 | 91 | if len(filename) == 0 { 92 | filename = fmt.Sprintf("new%d", newtabiter) 93 | newtabiter++ 94 | } else { 95 | t.Filename = filename 96 | } 97 | 98 | if len(t.Filename) > 0 { 99 | ct := ui.GetCurrentTab() 100 | if ct != nil && len(ct.Filename) == 0 && !ct.Dirty { 101 | ct.Close() 102 | } 103 | } 104 | 105 | t.swin = gtk.NewScrolledWindow(nil, nil) 106 | t.swin.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) 107 | t.swin.SetShadowType(gtk.SHADOW_IN) 108 | 109 | t.sourcebuffer = gsv.NewSourceBuffer() 110 | t.sourceview = gsv.NewSourceViewWithBuffer(t.sourcebuffer) 111 | 112 | t.DragAndDrop() 113 | 114 | t.swin.Add(t.sourceview) 115 | 116 | t.label = gtk.NewLabel(path.Base(filename)) 117 | t.label.SetTooltipText(filename) 118 | 119 | t.tab = gtk.NewHBox(false, 0) 120 | t.tab.PackStart(t.label, true, true, 0) 121 | 122 | if len(t.Filename) > 0 { 123 | 124 | stat, err := os.Stat(filename) 125 | if err == nil && !stat.IsDir() { 126 | 127 | text, err := t.ReadFile(filename) 128 | if err != nil { 129 | errorMessage(err) 130 | log.Println(err) 131 | return 132 | } 133 | 134 | t.sourcebuffer.BeginNotUndoableAction() 135 | t.sourcebuffer.SetText(text) 136 | t.sourcebuffer.EndNotUndoableAction() 137 | } 138 | } 139 | 140 | if issetLanguage(t.Language) { 141 | t.sourcebuffer.SetLanguage(langManager.GetLanguage(t.Language)) 142 | } 143 | 144 | t.ApplyConf() 145 | 146 | // t.tab.ShowAll() 147 | 148 | t.eventbox = gtk.NewEventBox() 149 | t.eventbox.Connect("button_press_event", t.onTabPress) 150 | t.eventbox.Add(t.tab) 151 | t.eventbox.ShowAll() 152 | 153 | t.sourcebuffer.Connect("changed", t.onchange) 154 | t.sourcebuffer.Connect("notify::cursor-moved", t.onMoveCursor) // notify::cursor-position for the old gtksourcebuffer 155 | 156 | return t 157 | } 158 | 159 | func (t *Tab) ApplyConf() { 160 | t.sourcebuffer.SetStyleScheme(conf.schemeManager.GetScheme(conf.TextView.StyleScheme)) 161 | 162 | t.sourceview.SetHighlightCurrentLine(conf.TextView.LineHightlight) 163 | t.sourceview.ModifyFontEasy(conf.TextView.Font) 164 | t.sourceview.SetShowLineNumbers(conf.TextView.LineNumbers) 165 | 166 | if len(t.Filename) == 0 { 167 | t.SetTabFGColor(conf.Tabs.FGNew) 168 | } else { 169 | t.SetTabFGColor(conf.Tabs.FGNormal) 170 | } 171 | 172 | t.tab.SetSizeRequest(-1, conf.Tabs.Height) 173 | 174 | if conf.Tabs.CloseBtns { 175 | if t.closeBtn == nil { 176 | t.closeBtn = gtk.NewButton() 177 | t.closeBtn.Add(gtk.NewImageFromStock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_BUTTON)) 178 | t.closeBtn.SetRelief(gtk.RELIEF_NONE) 179 | t.closeBtn.Clicked(t.close) 180 | t.tab.PackStart(t.closeBtn, false, false, 0) 181 | } 182 | 183 | t.closeBtn.ShowAll() 184 | t.closeBtn.SetSizeRequest(conf.Tabs.Height, conf.Tabs.Height) 185 | } else { 186 | if t.closeBtn != nil { 187 | t.closeBtn.HideAll() 188 | } 189 | } 190 | 191 | if t.Encoding != CHARSET_BINARY { 192 | t.sourceview.SetTabWidth(uint(conf.TextView.IndentWidth)) 193 | t.sourceview.SetInsertSpacesInsteadOfTabs(conf.TextView.IndentSpace) 194 | 195 | if conf.TextView.WordWrap { 196 | t.sourceview.SetWrapMode(gtk.WRAP_WORD_CHAR) 197 | } 198 | } 199 | 200 | } 201 | 202 | func (t *Tab) UpdateMenuSeleted() { 203 | ui.NoActivate = true 204 | if ra, ok := ui.encodings[t.Encoding]; ok { 205 | ra.SetActive(true) 206 | } 207 | 208 | if ra, ok := ui.languages[t.Language]; ok { 209 | ra.SetActive(true) 210 | } 211 | ui.NoActivate = false 212 | } 213 | 214 | func (t *Tab) onTabPress(ctx *glib.CallbackContext) { 215 | arg := ctx.Args(0) 216 | event := *(**gdk.EventButton)(unsafe.Pointer(&arg)) 217 | 218 | if event.Button == 2 { 219 | if _, n, ok := ui.LookupTab(t.Filename); ok { 220 | ui.CloseTab(n) 221 | return 222 | } 223 | } 224 | } 225 | 226 | func (t *Tab) close() { 227 | if _, n, ok := ui.LookupTab(t.Filename); ok { 228 | ui.CloseTab(n) 229 | } 230 | } 231 | 232 | func (t *Tab) Close() { 233 | if t.File != nil { 234 | t.File.Close() 235 | } 236 | 237 | t = nil 238 | } 239 | 240 | func (t *Tab) ReadFile(filename string) (string, error) { 241 | var err error 242 | t.File, err = os.Open(filename) 243 | if err != nil { 244 | err := fmt.Errorf("failed open file `%s`, %s", filename, err) 245 | return "", err 246 | } 247 | defer t.File.Close() 248 | 249 | data, err := ioutil.ReadAll(t.File) 250 | if err != nil { 251 | err := fmt.Errorf("failed read file `%s`, %s", filename, err) 252 | return "", err 253 | } 254 | 255 | if len(data) > 0 { 256 | t.Encoding, err = t.DetectEncoding(data) 257 | if err != nil { 258 | t.Encoding = CHARSET_BINARY 259 | } 260 | 261 | if t.Encoding != CHARSET_UTF8 && t.Encoding != CHARSET_BINARY { 262 | newdata, err := t.ChangeEncoding(data, CHARSET_UTF8, t.Encoding) 263 | if err != nil { 264 | errorMessage(err) 265 | t.Encoding = CHARSET_BINARY 266 | } else { 267 | data = newdata 268 | } 269 | 270 | } 271 | 272 | if t.Encoding != CHARSET_BINARY { 273 | t.Language = t.DetectLanguage(data) 274 | return string(data), nil 275 | } 276 | 277 | if issetLanguage("hex") { 278 | t.Language = "hex" 279 | } 280 | 281 | return bytetohex(bytes.NewReader(data)), nil 282 | } 283 | return "", nil 284 | } 285 | 286 | const CHARSET_BINARY = "binary" 287 | const CHARSET_UTF8 = "utf-8" 288 | const CHARSET_ASCII = "ascii" 289 | 290 | func (t *Tab) DetectEncoding(data []byte) (string, error) { 291 | httpContentType := http.DetectContentType(data) 292 | if !strings.HasPrefix(httpContentType, "text") { 293 | return CHARSET_BINARY, nil 294 | } 295 | 296 | contentType := strings.Split(httpContentType, ";") 297 | if len(contentType) != 2 { 298 | c, err := t.DetectChardet(data) 299 | if err != nil { 300 | return "", errors.New("failed split content type amd detect charset") 301 | } 302 | return c, nil 303 | } 304 | 305 | charset := strings.Split(contentType[1], "=") 306 | if len(charset) != 2 { 307 | return "", errors.New("failed split charset") 308 | } 309 | 310 | if charset[1] == CHARSET_UTF8 && !utf8.Valid(data) { 311 | return t.DetectChardet(data) 312 | } 313 | 314 | return charset[1], nil 315 | } 316 | 317 | func (t *Tab) DetectChardet(data []byte) (string, error) { 318 | // log.Println(chardet.NewTextDetector().DetectAll(data)) 319 | r, err := chardet.NewTextDetector().DetectBest(data) 320 | if err != nil || r.Confidence < 30 { 321 | return "", errors.New("failed detect charset with chardet") 322 | } 323 | return r.Charset, nil 324 | } 325 | 326 | func (t *Tab) ChangeEncoding(data []byte, to, from string) ([]byte, error) { 327 | converter, err := iconv.NewConverter(from, to) 328 | if err != nil { 329 | return nil, fmt.Errorf("unknown charsets: `%s` `%s`, %s", to, from, err) 330 | } 331 | 332 | newdata := make([]byte, len(data)*4) 333 | _, n, err := converter.Convert(data, newdata) 334 | if err != nil { 335 | return nil, fmt.Errorf("failed change encoding from `%s`, %s", from, err) 336 | } 337 | 338 | return newdata[:n], nil 339 | 340 | // cd, err := iconv.Open(to, from) 341 | // if err != nil { 342 | // return nil, fmt.Errorf("unknown charsets: `%s` `%s`, %s", to, from, err) 343 | // } 344 | // defer cd.Close() 345 | 346 | // var outbuf = make([]byte, len(data)) 347 | // out, _, err := cd.Conv(data, outbuf) 348 | // if err != nil { 349 | // return nil, fmt.Errorf("failed change encoding from `%s`, %s", from, err) 350 | // } 351 | // return out, nil 352 | } 353 | 354 | func (t *Tab) ChangeCurrEncoding(from string) { 355 | if t == nil { 356 | return 357 | } 358 | 359 | // log.Println("ChangeCurrEncoding", t.Filename, t.Encoding, from) 360 | 361 | if t.Encoding == from { 362 | return 363 | } 364 | 365 | var data []byte 366 | var err error 367 | 368 | dirtyState := t.Dirty 369 | 370 | var tmpdata []byte 371 | if t.Dirty || t.File == nil { 372 | tmpdata = []byte(t.GetText(true)) 373 | } else { 374 | tmpdata, err = ioutil.ReadFile(t.Filename) 375 | if err != nil { 376 | errorMessage(err) 377 | log.Println(err) 378 | return 379 | } 380 | } 381 | 382 | if t.Dirty { 383 | if t.Encoding == CHARSET_BINARY { 384 | tmpdata = regexp.MustCompile("[ \n\r]+").ReplaceAll(tmpdata, []byte{}) 385 | data, err = hex.DecodeString(string(tmpdata)) 386 | } else { 387 | data, err = t.ChangeEncoding(tmpdata, t.Encoding, CHARSET_UTF8) 388 | } 389 | if err != nil { 390 | errorMessage(err) 391 | log.Println(err) 392 | return 393 | } 394 | } else { 395 | data = tmpdata 396 | } 397 | 398 | if from == CHARSET_BINARY { 399 | t.Language = CHARSET_BINARY 400 | data = []byte(bytetohex(bytes.NewReader(data))) 401 | } else { 402 | data, err = t.ChangeEncoding(data, CHARSET_UTF8, from) 403 | if err != nil { 404 | log.Println(err) 405 | errorMessage(err) 406 | return 407 | } 408 | } 409 | 410 | t.Encoding = from 411 | if t.sourcebuffer != nil { 412 | t.sourcebuffer.SetText(string(data)) 413 | } 414 | t.Dirty = dirtyState 415 | } 416 | 417 | func (t *Tab) ChangeLanguage(lang string) { 418 | if t == nil { 419 | return 420 | } 421 | 422 | if t.Language == lang { 423 | return 424 | } 425 | 426 | t.Language = lang 427 | if t.sourcebuffer != nil { 428 | t.sourcebuffer.SetLanguage(langManager.GetLanguage(lang)) 429 | } 430 | } 431 | 432 | func (t *Tab) DetectLanguage(data []byte) string { 433 | if len(languages) == 0 { 434 | return "" 435 | } 436 | 437 | ext := path.Ext(t.Filename) 438 | if len(ext) > 0 { 439 | ext = ext[1:] 440 | } 441 | if issetLanguage(ext) { 442 | return ext 443 | } 444 | 445 | if strings.HasSuffix(t.Filename, "rc") { 446 | return "sh" 447 | } 448 | 449 | size := 64 450 | if size > len(data) { 451 | size = len(data) 452 | } 453 | line := string(bytes.SplitN(data[:size], []byte("\n"), 2)[0]) 454 | _line := strings.Split(line, " ") 455 | if issetLanguage(_line[len(_line)-1]) { 456 | return _line[len(_line)-1] 457 | } 458 | 459 | _, f := path.Split(_line[0]) 460 | if issetLanguage(f) { 461 | return f 462 | } 463 | 464 | maybexml := strings.Trim(_line[0], " 0 { 471 | return strings.ToLower(name) 472 | } 473 | 474 | scanner := bufio.NewScanner(bytes.NewReader(data)) 475 | for scanner.Scan() { 476 | line := scanner.Bytes() 477 | if len(line) == 0 { 478 | continue 479 | } 480 | 481 | if line[0] == '#' { 482 | if issetLanguage("toml") { 483 | return "toml" 484 | } 485 | if issetLanguage("yaml") { 486 | return "yaml" 487 | } 488 | if issetLanguage("sh") { 489 | return "sh" 490 | } 491 | // if issetLanguage("desktop") { 492 | // return "desktop" 493 | // } 494 | } 495 | 496 | if line[0] == ';' { 497 | if issetLanguage("ini") { 498 | return "ini" 499 | } 500 | } 501 | 502 | if line[0] == '[' { 503 | if issetLanguage("ini") { 504 | return "ini" 505 | } 506 | if issetLanguage("toml") { 507 | return "toml" 508 | } 509 | } 510 | } 511 | // if ext == ".conf" || ext == ".cfg" { 512 | // if issetLanguage("ini") { 513 | // return "ini" 514 | // } 515 | // if issetLanguage("toml") { 516 | // return "toml" 517 | // } 518 | // } 519 | 520 | return "sh" 521 | } 522 | 523 | func (t *Tab) DragAndDrop() { 524 | t.sourceview.DragDestAddUriTargets() 525 | t.sourceview.Connect("drag-data-received", t.DnDHandler) 526 | } 527 | 528 | func (t *Tab) DnDHandler(ctx *glib.CallbackContext) { 529 | sdata := gtk.NewSelectionDataFromNative(unsafe.Pointer(ctx.Args(3))) 530 | if sdata != nil { 531 | a := (*[2048]uint8)(sdata.GetData()) 532 | files := strings.Split(string(a[:sdata.GetLength()-1]), "\n") 533 | for _, filename := range files { 534 | filename = resolveFilename(filename[:len(filename)-1]) 535 | ui.NewTab(filename) 536 | } 537 | } 538 | } 539 | 540 | func (t *Tab) onchange() { 541 | // t.Data = t.GetText() 542 | t.Dirty = true 543 | t.SetTabFGColor(conf.Tabs.FGModified) 544 | 545 | t.Find() 546 | // t.Empty = false 547 | } 548 | 549 | func (t *Tab) SetTabFGColor(col []int) { 550 | color := convertColor(col) 551 | t.label.ModifyFG(gtk.STATE_NORMAL, color) 552 | t.label.ModifyFG(gtk.STATE_PRELIGHT, color) 553 | t.label.ModifyFG(gtk.STATE_SELECTED, color) 554 | t.label.ModifyFG(gtk.STATE_ACTIVE, color) 555 | } 556 | 557 | func (t *Tab) Save() { 558 | var err error 559 | var data []byte 560 | if t.Encoding == CHARSET_BINARY { 561 | 562 | data, err = hextobyte(t.GetText(false)) 563 | if err != nil { 564 | err := fmt.Errorf("failed decode hex, %s", err) 565 | errorMessage(err) 566 | log.Println(err) 567 | return 568 | } 569 | 570 | } else if t.ReadOnly { 571 | 572 | err := fmt.Errorf("file %s is read only", t.Filename) 573 | errorMessage(err) 574 | log.Println(err) 575 | return 576 | 577 | } else if t.Encoding == CHARSET_ASCII || t.Encoding == CHARSET_UTF8 { 578 | 579 | data = []byte(t.GetText(true)) 580 | 581 | } else { 582 | 583 | data, err = t.ChangeEncoding([]byte(t.GetText(true)), t.Encoding, "utf-8") 584 | if err != nil { 585 | err := fmt.Errorf("failed restore encoding, save failed, %s", err) 586 | errorMessage(err) 587 | log.Println(err) 588 | return 589 | } 590 | 591 | } 592 | 593 | if err := ioutil.WriteFile(t.Filename, data, 0644); err != nil { 594 | err := fmt.Errorf("failed save file `%s`, %s", t.Filename, err) 595 | errorMessage(err) 596 | log.Println(err) 597 | return 598 | } 599 | 600 | t.SetTabFGColor(conf.Tabs.FGNormal) 601 | } 602 | 603 | func (t *Tab) GetText(hiddenChars bool) string { 604 | if t.sourcebuffer == nil { 605 | return "" 606 | } 607 | 608 | var start gtk.TextIter 609 | var end gtk.TextIter 610 | 611 | t.sourcebuffer.GetStartIter(&start) 612 | t.sourcebuffer.GetEndIter(&end) 613 | return t.sourcebuffer.GetText(&start, &end, hiddenChars) 614 | } 615 | 616 | func (t *Tab) ClearFind() { 617 | t.find = "" 618 | t.findtext = "" 619 | t.findindex = nil 620 | t.findindexCurrent = 0 621 | 622 | tabletag := t.sourcebuffer.GetTagTable() 623 | 624 | if tag := tabletag.Lookup("find"); tag != nil && tag.GTextTag != nil { 625 | tabletag.Remove(tag) 626 | } 627 | 628 | if tag := tabletag.Lookup("findCurr"); tag != nil && tag.GTextTag != nil { 629 | tabletag.Remove(tag) 630 | } 631 | } 632 | 633 | func (t *Tab) Find() { 634 | t.ClearFind() 635 | 636 | t.find = ui.footer.findEntry.GetText() 637 | if len(t.find) == 0 || !ui.footer.table.GetVisible() { 638 | t.tagfind = nil 639 | t.tagfindCurrent = nil 640 | return 641 | } 642 | 643 | if !ui.footer.table.GetVisible() { 644 | return 645 | } 646 | 647 | flags := "ms" 648 | if !ui.footer.caseBtn.GetActive() { 649 | flags += "i" 650 | } 651 | 652 | if !ui.footer.regBtn.GetActive() { 653 | t.find = regexp.QuoteMeta(t.find) 654 | } 655 | 656 | if t.Encoding == "binary" { 657 | t.find = regexp.MustCompile("[ \n\r]+").ReplaceAllString(t.find, "") 658 | t.find = regexp.MustCompile("(?i)([0-9a-z]{2})").ReplaceAllString(t.find, "$1[ \r\n]*") 659 | } 660 | 661 | findtext := t.GetText(true) 662 | 663 | //if new text not found prev, reset index 664 | if findtext != t.findtext { 665 | t.findindexCurrent = 0 666 | } 667 | t.findtext = findtext 668 | 669 | expr := fmt.Sprintf("(?%s)%s", flags, t.find) 670 | reg, err := regexp.Compile(expr) 671 | if err != nil { 672 | log.Println("invalid search query,", err) 673 | return 674 | } 675 | 676 | t.findindex = reg.FindAllStringIndex(t.findtext, conf.Search.MaxItems) 677 | 678 | t.tagfind = t.sourcebuffer.CreateTag("find", map[string]interface{}{"background": "#999999"}) 679 | t.tagfindCurrent = t.sourcebuffer.CreateTag("findCurr", map[string]interface{}{"background": "#eeaa00"}) 680 | 681 | for i, index := range t.findindex { 682 | data := []byte(t.findtext) 683 | if t.Encoding != "binary" { 684 | index[0] = utf8.RuneCount(data[:index[0]]) 685 | index[1] = utf8.RuneCount(data[:index[1]]) 686 | t.findindex[i] = index 687 | } 688 | if i == 0 { 689 | t.Highlight(i, true) 690 | } else { 691 | t.Highlight(i, false) 692 | } 693 | } 694 | } 695 | 696 | func (t *Tab) onMoveCursor() { 697 | mark := t.sourcebuffer.GetInsert() 698 | t.sourcebuffer.GetIterAtMark(&t.cursorPos, mark) 699 | t.findoffset = t.cursorPos.GetOffset() 700 | t.findwrap = false 701 | 702 | t.Highlight(t.findindexCurrent, false) 703 | t.findindexCurrent = -1 704 | } 705 | 706 | func (t *Tab) FindNext(next bool) { 707 | if len(t.findindex) < 2 { 708 | return 709 | } 710 | 711 | if t.findindexCurrent > len(t.findindex) { 712 | t.findindexCurrent = len(t.findindex) - 1 713 | } 714 | 715 | t.Highlight(t.findindexCurrent, false) 716 | 717 | if next { 718 | t.findindexCurrent++ 719 | if t.findindexCurrent >= len(t.findindex) { 720 | t.findindexCurrent = 0 721 | t.findwrap = true 722 | } 723 | 724 | } else { 725 | t.findindexCurrent-- 726 | if t.findindexCurrent < 0 { 727 | t.findindexCurrent = len(t.findindex) - 1 728 | } 729 | } 730 | 731 | index := t.findindex[t.findindexCurrent] 732 | if !t.findwrap { 733 | for index[1] < t.findoffset { 734 | t.findindexCurrent++ 735 | if t.findindexCurrent >= len(t.findindex) { 736 | t.findindexCurrent = 0 737 | t.findwrap = true 738 | break 739 | } 740 | index = t.findindex[t.findindexCurrent] 741 | } 742 | } 743 | 744 | t.Highlight(t.findindexCurrent, true) 745 | } 746 | 747 | func (t *Tab) Highlight(i int, current bool) { 748 | if i >= len(t.findindex) || i < 0 { 749 | return 750 | } 751 | index := t.findindex[i] 752 | var start gtk.TextIter 753 | var end gtk.TextIter 754 | t.sourcebuffer.GetIterAtOffset(&start, index[0]) 755 | t.sourcebuffer.GetIterAtOffset(&end, index[1]) 756 | 757 | if current { 758 | t.sourcebuffer.RemoveTag(t.tagfind, &start, &end) 759 | t.sourcebuffer.ApplyTag(t.tagfindCurrent, &start, &end) 760 | t.Scroll(start) 761 | } else { 762 | t.sourcebuffer.RemoveTag(t.tagfindCurrent, &start, &end) 763 | t.sourcebuffer.ApplyTag(t.tagfind, &start, &end) 764 | } 765 | } 766 | 767 | // func (t *Tab) RemoveTag(name string) { 768 | // tagtable := t.sourcebuffer.GetTagTable() 769 | // if tag := tagtable.Lookup(name); tag != nil { 770 | // tagtable.Remove(tag) 771 | // } 772 | // } 773 | 774 | func (t *Tab) Scroll(iter gtk.TextIter) { 775 | // log.Println(iter.GetOffset()) 776 | t.sourceview.ScrollToIter(&iter, 0, false, 0, 0) 777 | } 778 | 779 | func (t *Tab) Replace(all bool) { 780 | var n = 1 781 | if all { 782 | n = -1 783 | } 784 | 785 | if t.Encoding != "binary" { 786 | t.replaceInText(n) 787 | } else { 788 | t.replaceInHex(n) 789 | } 790 | } 791 | 792 | func (t *Tab) replaceInText(n int) { 793 | findtext := ui.footer.findEntry.GetText() 794 | repltext := ui.footer.replEntry.GetText() 795 | 796 | if ui.footer.caseBtn.GetActive() { 797 | findtext = strings.ToLower(findtext) 798 | } 799 | 800 | var text string 801 | if ui.footer.regBtn.GetActive() { 802 | reg, err := regexp.Compile("(?m)" + findtext) 803 | if err != nil { 804 | log.Println("failed compile regexp", err) 805 | return 806 | } 807 | log.Println("regexp always replace all occurrences") 808 | text = reg.ReplaceAllString(t.GetText(true), repltext) 809 | } else { 810 | text = strings.Replace(t.GetText(true), findtext, repltext, n) 811 | } 812 | 813 | t.sourcebuffer.SetText(text) 814 | 815 | t.Find() 816 | } 817 | 818 | func (t *Tab) replaceInHex(n int) { 819 | find, err := hextobyte(ui.footer.findEntry.GetText()) 820 | if err != nil { 821 | log.Println("invalid hex string", err) 822 | return 823 | } 824 | repl, err := hextobyte(ui.footer.replEntry.GetText()) 825 | if err != nil { 826 | log.Println("invalid hex string", err) 827 | return 828 | } 829 | 830 | data, err := hextobyte(t.GetText(false)) 831 | if err != nil { 832 | log.Println("invalid hex string", err) 833 | return 834 | } 835 | 836 | data = bytes.Replace(data, find, repl, n) 837 | 838 | t.sourcebuffer.SetText(string(data)) 839 | 840 | text := bytetohex(bytes.NewReader(data)) 841 | t.sourcebuffer.SetText(text) 842 | t.Find() 843 | } 844 | 845 | func hextobyte(hexstr string) ([]byte, error) { 846 | hexstr = regexp.MustCompile("(?m)[ \n\r]").ReplaceAllString(hexstr, "") 847 | return hex.DecodeString(hexstr) 848 | } 849 | 850 | func bytetohex(r io.Reader) string { 851 | var dump []string 852 | var line = make([]byte, conf.Hex.BytesInLine) 853 | for { 854 | n, err := r.Read(line) 855 | if err != nil && err != io.EOF { 856 | err := fmt.Errorf("failed read file %s", err) 857 | errorMessage(err) 858 | log.Println(err) 859 | break 860 | } 861 | 862 | line = line[:n] 863 | if err == io.EOF { 864 | break 865 | } 866 | 867 | dump = append(dump, fmt.Sprintf("% x", line)) 868 | } 869 | 870 | return strings.Join(dump, "\n") 871 | } 872 | -------------------------------------------------------------------------------- /goatee_ui.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "path" 7 | "strconv" 8 | "unsafe" 9 | 10 | "github.com/mattn/go-gtk/gdk" 11 | "github.com/mattn/go-gtk/glib" 12 | "github.com/mattn/go-gtk/gtk" 13 | ) 14 | 15 | type UI struct { 16 | window *gtk.Window 17 | vbox *gtk.VBox 18 | 19 | menu *Menu 20 | notebook *gtk.Notebook 21 | tabs []*Tab 22 | footer *Footer 23 | 24 | NoActivate bool 25 | encodings map[string]*gtk.RadioAction 26 | languages map[string]*gtk.RadioAction 27 | } 28 | 29 | func CreateUI() *UI { 30 | ui := new(UI) 31 | ui.window = gtk.NewWindow(gtk.WINDOW_TOPLEVEL) 32 | ui.window.SetDefaultSize(600, 300) 33 | ui.window.SetSizeRequest(100, 100) 34 | ui.window.SetIconName("accessories-text-editor") 35 | 36 | ui.menu = NewMenu(ui.window) 37 | ui.footer = NewFooter(ui.menu.accelGroup) 38 | ui.SetActions() 39 | 40 | ui.vbox = gtk.NewVBox(false, 0) 41 | ui.vbox.PackStart(ui.menu.GetMenubar(), false, false, 0) 42 | 43 | ui.notebook = gtk.NewNotebook() 44 | ui.notebook.Connect("switch-page", ui.onSwitchPage) 45 | ui.notebook.Connect("page-reordered", ui.onPageReordered) 46 | ui.vbox.PackStart(ui.notebook, true, true, 0) 47 | 48 | ui.vbox.PackStart(ui.footer.table, false, false, 0) 49 | ui.window.Add(ui.vbox) 50 | 51 | ui.window.Connect("destroy", ui.Quit) 52 | 53 | ui.window.ShowAll() 54 | 55 | ui.footer.table.SetVisible(false) 56 | ui.menu.menubar.SetVisible(conf.UI.MenuBarVisible) 57 | 58 | return ui 59 | } 60 | 61 | type Menu struct { 62 | uiManager *gtk.UIManager 63 | accelGroup *gtk.AccelGroup 64 | actionGroup *gtk.ActionGroup 65 | 66 | menubar *gtk.Widget 67 | } 68 | 69 | func NewMenu(w *gtk.Window) *Menu { 70 | UIxml := ` 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | ` + xmlEncodings() + ` 82 | 83 | 84 | ` + xmlLanguages() + ` 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | ` 110 | 111 | uiman := gtk.NewUIManager() 112 | uiman.AddUIFromString(UIxml) 113 | 114 | accels := uiman.GetAccelGroup() 115 | w.AddAccelGroup(accels) 116 | 117 | actions := gtk.NewActionGroup("my_group") 118 | uiman.InsertActionGroup(actions, 0) 119 | 120 | actions.AddAction(gtk.NewAction("File", "File", "", "")) 121 | actions.AddAction(gtk.NewAction("Edit", "Edit", "", "")) 122 | actions.AddAction(gtk.NewAction("View", "View", "", "")) 123 | 124 | return &Menu{ 125 | uiManager: uiman, 126 | accelGroup: accels, 127 | actionGroup: actions, 128 | } 129 | } 130 | 131 | func (menu *Menu) GetMenubar() *gtk.Widget { 132 | menu.menubar = menu.uiManager.GetWidget("/MenuBar") 133 | return menu.menubar 134 | } 135 | 136 | func (ui *UI) SetActions() { 137 | // File 138 | ui.newAction("NewTab", "New Tab", "t", func() { ui.NewTab("") }) 139 | ui.newActionStock("Open", gtk.STOCK_OPEN, "", ui.Open) 140 | ui.newActionStock("Save", gtk.STOCK_SAVE, "", ui.Save) 141 | ui.newActionStock("SaveAs", gtk.STOCK_SAVE_AS, "s", ui.SaveAs) 142 | 143 | //Encodings 144 | ui.newAction("Encoding", "Encoding", "", nil) 145 | ui.encodings = make(map[string]*gtk.RadioAction) 146 | var encodingsGroup *glib.SList 147 | for n, c := range charsets { 148 | if len(c) != 0 { 149 | ra := ui.newRadioAction(c, c, "", false, n, ui.changeEncodingCurrentTab, c) 150 | ra.SetGroup(encodingsGroup) 151 | encodingsGroup = ra.GetGroup() 152 | ui.encodings[c] = ra 153 | } 154 | } 155 | 156 | //Languages 157 | ui.newAction("Language", "Language", "", nil) 158 | ui.languages = make(map[string]*gtk.RadioAction) 159 | var langGroup *glib.SList 160 | for section, langs := range structureLanguages() { 161 | ui.newAction(section, section, "", nil) 162 | for _, l := range langs { 163 | ra := ui.newRadioAction(l.name, l.name, "", false, l.n, ui.changeLanguageCurrentTab, l.name) 164 | ra.SetGroup(langGroup) 165 | langGroup = ra.GetGroup() 166 | ui.languages[l.name] = ra 167 | } 168 | } 169 | 170 | ui.newAction("CloseTab", "Close Tab", "w", ui.CloseCurrentTab) 171 | ui.newActionStock("Quit", gtk.STOCK_QUIT, "", ui.Quit) 172 | 173 | // Edit 174 | ui.newActionStock("Find", gtk.STOCK_FIND, "", ui.footer.ShowFindbar) 175 | ui.newAction("FindNext", "Find Next", "F3", ui.FindNext) 176 | ui.newAction("FindPrev", "Find Previous", "F3", ui.FindPrev) 177 | 178 | ui.newActionStock("Replace", gtk.STOCK_FIND_AND_REPLACE, "h", ui.footer.ShowReplbar) 179 | ui.newAction("ReplaceOne", "Replace One", "h", ui.ReplaceOne) 180 | ui.newAction("ReplaceAll", "Replace All", "Return", ui.ReplaceAll) 181 | ui.newAction("Preferences", "Preferences", "p", conf.OpenWindow) 182 | 183 | // View 184 | ui.newToggleAction("Menubar", "Menubar", "M", conf.UI.MenuBarVisible, ui.ToggleMenuBar) 185 | 186 | // Footer 187 | ui.footer.regBtn.Connect("toggled", ui.Find) 188 | ui.footer.caseBtn.Connect("toggled", ui.Find) 189 | ui.footer.findEntry.Connect("changed", ui.Find) 190 | ui.footer.findNextBtn.Clicked(ui.FindNext) 191 | ui.footer.findPrevBtn.Clicked(ui.FindPrev) 192 | ui.footer.closeBtn.Clicked(ui.FooterClose) 193 | ui.footer.closeBtn.AddAccelerator("activate", ui.menu.accelGroup, gdk.KEY_Escape, 0, gtk.ACCEL_VISIBLE) 194 | ui.footer.replBtn.Clicked(ui.ReplaceOne) 195 | ui.footer.replAllBtn.Clicked(ui.ReplaceAll) 196 | } 197 | 198 | func (ui *UI) newAction(dst, label, accel string, f interface{}, vars ...interface{}) { 199 | action := gtk.NewAction(dst, label, "", "") 200 | if f != nil { 201 | action.Connect("activate", f, vars...) 202 | } 203 | ui.menu.actionGroup.AddActionWithAccel(action, accel) 204 | } 205 | 206 | func (ui *UI) newActionStock(dst, stock, accel string, f interface{}, vars ...interface{}) { 207 | action := gtk.NewAction(dst, "", "", stock) 208 | action.Connect("activate", f, vars...) 209 | ui.menu.actionGroup.AddActionWithAccel(action, accel) 210 | } 211 | 212 | func (ui *UI) newToggleAction(dst, label, accel string, state bool, f func()) { 213 | action := gtk.NewToggleAction(dst, label, "", "") 214 | action.SetActive(state) 215 | action.Connect("activate", f) 216 | ui.menu.actionGroup.AddActionWithAccel(&action.Action, accel) 217 | } 218 | 219 | func (ui *UI) newRadioAction(dst, label, accel string, state bool, n int, f interface{}, vars ...interface{}) *gtk.RadioAction { 220 | action := gtk.NewRadioAction(dst, label, "", "", n) 221 | action.SetActive(state) 222 | action.Connect("changed", f, vars...) 223 | ui.menu.actionGroup.AddActionWithAccel(&action.Action, accel) 224 | return action 225 | } 226 | 227 | func (ui *UI) NewTab(filename string) { 228 | t := NewTab(filename) 229 | if t == nil { 230 | return 231 | } 232 | 233 | n := ui.notebook.AppendPage(t.swin, t.eventbox) 234 | ui.notebook.ShowAll() 235 | ui.notebook.SetCurrentPage(n) 236 | 237 | ui.notebook.ChildSet(t.swin, "tab-expand", conf.Tabs.Homogeneous) 238 | ui.notebook.SetReorderable(t.swin, true) 239 | 240 | t.sourceview.GrabFocus() 241 | t.UpdateMenuSeleted() 242 | 243 | ui.tabs = append(ui.tabs, t) 244 | } 245 | 246 | func (ui *UI) ShowTab(t *Tab) { 247 | log.Println("ShowTab", t.Filename) 248 | for _, uitab := range ui.tabs { 249 | uitab.swin.Hide() 250 | } 251 | t.swin.ShowAll() 252 | } 253 | 254 | func (ui *UI) TabsUpdateConf() { 255 | for _, t := range ui.tabs { 256 | t.ApplyConf() 257 | } 258 | } 259 | 260 | func (ui *UI) Open() { 261 | dialog := gtk.NewFileChooserDialog("Open File", ui.window, gtk.FILE_CHOOSER_ACTION_OPEN, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_OPEN, gtk.RESPONSE_ACCEPT) 262 | 263 | if dialog.Run() == gtk.RESPONSE_ACCEPT { 264 | ui.NewTab(dialog.GetFilename()) 265 | } 266 | dialog.Destroy() 267 | } 268 | func (ui *UI) Save() { 269 | t := ui.GetCurrentTab() 270 | 271 | if len(t.Filename) == 0 { 272 | filename := dialogSave() 273 | if len(filename) == 0 { 274 | return 275 | } 276 | 277 | t.Filename = filename 278 | t.label.SetText(path.Base(filename)) 279 | t.label.SetTooltipText(filename) 280 | } 281 | t.Save() 282 | } 283 | func (ui *UI) SaveAs() { 284 | t := ui.GetCurrentTab() 285 | 286 | filename := dialogSave() 287 | if len(filename) == 0 { 288 | return 289 | } 290 | 291 | t.Filename = filename 292 | t.label.SetText(path.Base(filename)) 293 | t.label.SetTooltipText(filename) 294 | t.Save() 295 | } 296 | 297 | func (ui *UI) Quit() { 298 | for _, t := range ui.tabs { 299 | t.File.Close() 300 | } 301 | gtk.MainQuit() 302 | } 303 | 304 | func (ui *UI) Find() { 305 | ui.GetCurrentTab().Find() 306 | } 307 | func (ui *UI) FindNext() { 308 | ui.GetCurrentTab().FindNext(true) 309 | } 310 | func (ui *UI) FindPrev() { 311 | ui.GetCurrentTab().FindNext(false) 312 | } 313 | func (ui *UI) ReplaceOne() { 314 | ui.GetCurrentTab().Replace(false) 315 | } 316 | func (ui *UI) ReplaceAll() { 317 | ui.GetCurrentTab().Replace(true) 318 | } 319 | 320 | func (ui *UI) ToggleMenuBar() { 321 | conf.UI.MenuBarVisible = !conf.UI.MenuBarVisible 322 | ui.menu.menubar.SetVisible(conf.UI.MenuBarVisible) 323 | 324 | } 325 | func (ui *UI) ToggleStatusBar() { 326 | log.Println("statusbar not yet ready") 327 | // conf.UI.StatusBarVisible = !conf.UI.StatusBarVisible 328 | // ui.statusbar.SetVisible(conf.UI.StatusBarVisible) 329 | // ui.menu.statusbar.SetActive(conf.UI.StatusBarVisible) 330 | } 331 | 332 | func dialogSave() string { 333 | dialog := gtk.NewFileChooserDialog("Save File", ui.window, gtk.FILE_CHOOSER_ACTION_SAVE, gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, gtk.STOCK_SAVE, gtk.RESPONSE_ACCEPT) 334 | 335 | var filename string 336 | if dialog.Run() == gtk.RESPONSE_ACCEPT { 337 | filename = dialog.GetFilename() 338 | } 339 | 340 | dialog.Destroy() 341 | 342 | return filename 343 | } 344 | 345 | func (ui *UI) changeEncodingCurrentTab(ctx *glib.CallbackContext) { 346 | if ui.NoActivate { 347 | return 348 | } 349 | charset := ctx.Data().(string) 350 | ui.GetCurrentTab().ChangeCurrEncoding(charset) 351 | } 352 | 353 | func (ui *UI) changeLanguageCurrentTab(ctx *glib.CallbackContext) { 354 | if ui.NoActivate { 355 | return 356 | } 357 | lang := ctx.Data().(string) 358 | ui.GetCurrentTab().ChangeLanguage(lang) 359 | } 360 | 361 | func (ui *UI) LookupTab(filename string) (*Tab, int, bool) { 362 | for n, t := range ui.tabs { 363 | if t.Filename == filename { 364 | // ui.notebook.SetCurrentPage(n) 365 | return t, n, true 366 | } 367 | } 368 | return nil, 0, false 369 | } 370 | 371 | func (ui *UI) CloseCurrentTab() { 372 | n := ui.notebook.GetCurrentPage() 373 | ui.CloseTab(n) 374 | } 375 | 376 | func (ui *UI) CloseTab(n int) { 377 | t := ui.tabs[n] 378 | 379 | ui.notebook.RemovePage(t.swin, n) 380 | t.Close() 381 | ui.tabs = append(ui.tabs[:n], ui.tabs[n+1:]...) 382 | 383 | if len(ui.tabs) == 0 { 384 | gtk.MainQuit() 385 | } 386 | } 387 | 388 | func (ui *UI) GetCurrentTab() *Tab { 389 | if ui.notebook == nil { 390 | return &Tab{} 391 | } 392 | 393 | n := ui.notebook.GetCurrentPage() 394 | if n < 0 { 395 | return nil 396 | } 397 | 398 | return ui.tabs[n] 399 | } 400 | 401 | func (ui *UI) onSwitchPage(ctx *glib.CallbackContext) { 402 | n, _ := strconv.Atoi(fmt.Sprintf("%v", ctx.Args(1))) 403 | if n < len(ui.tabs) { 404 | ui.tabs[n].UpdateMenuSeleted() 405 | } 406 | } 407 | 408 | func (ui *UI) onPageReordered(ctx *glib.CallbackContext) { 409 | child := *gtk.WidgetFromNative(unsafe.Pointer(ctx.Args(0))) 410 | i := int(ctx.Args(1)) 411 | 412 | for n, t := range ui.tabs { 413 | if child.GWidget == t.swin.Container.Widget.GWidget { 414 | ui.tabs[n], ui.tabs[i] = ui.tabs[i], ui.tabs[n] 415 | break 416 | } 417 | } 418 | } 419 | 420 | type Footer struct { 421 | table *gtk.Table 422 | 423 | findEntry *gtk.Entry 424 | replEntry *gtk.Entry 425 | 426 | regBtn *gtk.ToggleButton 427 | caseBtn *gtk.ToggleButton 428 | 429 | findNextBtn *gtk.Button 430 | findPrevBtn *gtk.Button 431 | 432 | replBtn *gtk.Button 433 | replAllBtn *gtk.Button 434 | 435 | closeBtn *gtk.Button 436 | } 437 | 438 | func NewFooter(accels *gtk.AccelGroup) *Footer { 439 | footer := new(Footer) 440 | 441 | footer.table = gtk.NewTable(2, 6, false) 442 | 443 | // findbar 444 | labelReg := gtk.NewLabel("Re") 445 | labelReg.ModifyFG(gtk.STATE_ACTIVE, gdk.NewColor("red")) 446 | footer.regBtn = gtk.NewToggleButton() 447 | footer.regBtn.Add(labelReg) 448 | 449 | labelCase := gtk.NewLabel("A") 450 | labelCase.ModifyFG(gtk.STATE_ACTIVE, gdk.NewColor("red")) 451 | footer.caseBtn = gtk.NewToggleButton() 452 | footer.caseBtn.Add(labelCase) 453 | footer.caseBtn.SetSizeRequest(20, 20) 454 | 455 | footer.findEntry = gtk.NewEntryWithBuffer(gtk.NewEntryBuffer("")) 456 | 457 | footer.findNextBtn = gtk.NewButton() 458 | footer.findNextBtn.SetSizeRequest(20, 20) 459 | footer.findNextBtn.Add(gtk.NewArrow(gtk.ARROW_DOWN, gtk.SHADOW_NONE)) 460 | 461 | footer.findPrevBtn = gtk.NewButton() 462 | footer.findPrevBtn.SetSizeRequest(20, 20) 463 | footer.findPrevBtn.Add(gtk.NewArrow(gtk.ARROW_UP, gtk.SHADOW_NONE)) 464 | 465 | footer.closeBtn = gtk.NewButton() 466 | footer.closeBtn.SetSizeRequest(20, 20) 467 | footer.closeBtn.Add(gtk.NewImageFromStock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_BUTTON)) 468 | 469 | // replacebar 470 | footer.replEntry = gtk.NewEntryWithBuffer(gtk.NewEntryBuffer("")) 471 | // footer.replEntry.Connect("changed", OnFindInput) 472 | 473 | footer.replBtn = gtk.NewButton() 474 | footer.replBtn.SetSizeRequest(20, 20) 475 | footer.replBtn.Add(gtk.NewImageFromIconName("text-changelog", gtk.ICON_SIZE_BUTTON)) 476 | 477 | footer.replAllBtn = gtk.NewButton() 478 | footer.replAllBtn.SetSizeRequest(20, 20) 479 | footer.replAllBtn.Add(gtk.NewImageFromIconName("text-plain", gtk.ICON_SIZE_BUTTON)) 480 | 481 | // btnRepl.Clicked(OnMenuFind) 482 | 483 | // pack to table 484 | footer.table.Attach(footer.regBtn, 0, 1, 0, 1, gtk.FILL, gtk.FILL, 0, 0) 485 | footer.table.Attach(footer.caseBtn, 1, 2, 0, 1, gtk.FILL, gtk.FILL, 0, 0) 486 | footer.table.Attach(footer.findEntry, 2, 3, 0, 1, gtk.EXPAND|gtk.FILL, gtk.FILL, 0, 0) 487 | footer.table.Attach(footer.findNextBtn, 3, 4, 0, 1, gtk.FILL, gtk.FILL, 0, 0) 488 | footer.table.Attach(footer.findPrevBtn, 4, 5, 0, 1, gtk.FILL, gtk.FILL, 0, 0) 489 | footer.table.Attach(footer.closeBtn, 5, 6, 0, 1, gtk.FILL, gtk.FILL, 0, 0) 490 | 491 | footer.table.Attach(footer.replEntry, 2, 3, 1, 2, gtk.EXPAND|gtk.FILL, gtk.FILL, 0, 0) 492 | footer.table.Attach(footer.replBtn, 3, 4, 1, 2, gtk.FILL, gtk.FILL, 0, 0) 493 | footer.table.Attach(footer.replAllBtn, 4, 5, 1, 2, gtk.FILL, gtk.FILL, 0, 0) 494 | 495 | return footer 496 | } 497 | 498 | // func (ui *UI) createFooter() *gtk.Table { 499 | // ui.footer.table = gtk.NewTable(2, 6, false) 500 | 501 | // // findbar 502 | // labelReg := gtk.NewLabel("Re") 503 | // labelReg.ModifyFG(gtk.STATE_ACTIVE, gdk.NewColor("red")) 504 | // ui.footer.regBtn = gtk.NewToggleButton() 505 | // ui.footer.regBtn.Add(labelReg) 506 | // ui.footer.regBtn.Connect("toggled", ui.Find) 507 | 508 | // labelCase := gtk.NewLabel("A") 509 | // labelCase.ModifyFG(gtk.STATE_ACTIVE, gdk.NewColor("red")) 510 | // ui.footer.caseBtn = gtk.NewToggleButton() 511 | // ui.footer.caseBtn.Add(labelCase) 512 | // ui.footer.caseBtn.SetSizeRequest(20, 20) 513 | // ui.footer.caseBtn.Connect("toggled", ui.Find) 514 | 515 | // ui.footer.findEntry = gtk.NewEntryWithBuffer(gtk.NewEntryBuffer("")) 516 | // ui.footer.findEntry.Connect("changed", ui.Find) 517 | 518 | // ui.footer.findNextBtn = gtk.NewButton() 519 | // ui.footer.findNextBtn.SetSizeRequest(20, 20) 520 | // ui.footer.findNextBtn.Add(gtk.NewArrow(gtk.ARROW_DOWN, gtk.SHADOW_NONE)) 521 | // ui.footer.findNextBtn.Clicked(ui.FindNext) 522 | 523 | // ui.footer.findPrevBtn = gtk.NewButton() 524 | // ui.footer.findPrevBtn.SetSizeRequest(20, 20) 525 | // ui.footer.findPrevBtn.Add(gtk.NewArrow(gtk.ARROW_UP, gtk.SHADOW_NONE)) 526 | // ui.footer.findPrevBtn.Clicked(ui.FindPrev) 527 | 528 | // ui.footer.closeBtn = gtk.NewButton() 529 | // ui.footer.closeBtn.SetSizeRequest(20, 20) 530 | // ui.footer.closeBtn.Add(gtk.NewImageFromStock(gtk.STOCK_CLOSE, gtk.ICON_SIZE_BUTTON)) 531 | // ui.footer.closeBtn.Clicked(ui.FooterClose) 532 | // ui.footer.closeBtn.AddAccelerator("activate", ui.accelGroup, gdk.KEY_Escape, 0, gtk.ACCEL_VISIBLE) 533 | 534 | // // replacebar 535 | // ui.footer.replEntry = gtk.NewEntryWithBuffer(gtk.NewEntryBuffer("")) 536 | // // ui.footer.replEntry.Connect("changed", OnFindInput) 537 | 538 | // ui.footer.replBtn = gtk.NewButton() 539 | // ui.footer.replBtn.SetSizeRequest(20, 20) 540 | // ui.footer.replBtn.Add(gtk.NewImageFromIconName("text-changelog", gtk.ICON_SIZE_BUTTON)) 541 | // ui.footer.replBtn.Clicked(ui.ReplaceOne) 542 | 543 | // ui.footer.replAllBtn = gtk.NewButton() 544 | // ui.footer.replAllBtn.SetSizeRequest(20, 20) 545 | // ui.footer.replAllBtn.Add(gtk.NewImageFromIconName("text-plain", gtk.ICON_SIZE_BUTTON)) 546 | // ui.footer.replAllBtn.Clicked(ui.ReplaceAll) 547 | // // btnRepl.Clicked(OnMenuFind) 548 | 549 | // // pack to table 550 | // ui.footer.table.Attach(ui.footer.regBtn, 0, 1, 0, 1, gtk.FILL, gtk.FILL, 0, 0) 551 | // ui.footer.table.Attach(ui.footer.caseBtn, 1, 2, 0, 1, gtk.FILL, gtk.FILL, 0, 0) 552 | // ui.footer.table.Attach(ui.footer.findEntry, 2, 3, 0, 1, gtk.EXPAND|gtk.FILL, gtk.FILL, 0, 0) 553 | // ui.footer.table.Attach(ui.footer.findNextBtn, 3, 4, 0, 1, gtk.FILL, gtk.FILL, 0, 0) 554 | // ui.footer.table.Attach(ui.footer.findPrevBtn, 4, 5, 0, 1, gtk.FILL, gtk.FILL, 0, 0) 555 | // ui.footer.table.Attach(ui.footer.closeBtn, 5, 6, 0, 1, gtk.FILL, gtk.FILL, 0, 0) 556 | 557 | // ui.footer.table.Attach(ui.footer.replEntry, 2, 3, 1, 2, gtk.EXPAND|gtk.FILL, gtk.FILL, 0, 0) 558 | // ui.footer.table.Attach(ui.footer.replBtn, 3, 4, 1, 2, gtk.FILL, gtk.FILL, 0, 0) 559 | // ui.footer.table.Attach(ui.footer.replAllBtn, 4, 5, 1, 2, gtk.FILL, gtk.FILL, 0, 0) 560 | 561 | // return ui.footer.table 562 | // } 563 | 564 | func (footer *Footer) ShowFindbar() { 565 | footer.table.SetVisible(true) 566 | footer.replEntry.SetVisible(false) 567 | footer.replBtn.SetVisible(false) 568 | footer.replAllBtn.SetVisible(false) 569 | 570 | footer.findEntry.GrabFocus() 571 | } 572 | 573 | func (footer *Footer) ShowReplbar() { 574 | footer.table.SetVisible(true) 575 | footer.replEntry.SetVisible(true) 576 | footer.replBtn.SetVisible(true) 577 | footer.replAllBtn.SetVisible(true) 578 | 579 | footer.replEntry.GrabFocus() 580 | } 581 | 582 | func (footer *Footer) Close() { 583 | footer.table.SetVisible(false) 584 | } 585 | 586 | func (ui *UI) FooterClose() { 587 | for _, t := range ui.tabs { 588 | t.ClearFind() 589 | } 590 | ui.footer.Close() 591 | } 592 | -------------------------------------------------------------------------------- /hex.lang: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | 25 | application/* 26 | *.hex;*.exe 27 | 28 | 29 | 30 |