├── 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 | 
20 |
21 |
22 | **binary file with hidden menu:**
23 |
24 | 
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, "
")
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], "#")
465 | if issetLanguage(maybexml) {
466 | return maybexml
467 | }
468 |
469 | name := gsv.NewSourceLanguageManager().GuessLanguage(t.Filename, "").GetName()
470 | if len(name) > 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 |
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 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | ^[0-9a-z]+
39 |
40 |
41 |
42 | \|.+$
43 |
44 |
45 |
46 | 00
47 |
48 |
49 |
50 | ff
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/screenshots/binary.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sg3des/goatee/0c9e012ce15e79221c4206bc06797552d5881098/screenshots/binary.png
--------------------------------------------------------------------------------
/screenshots/text.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sg3des/goatee/0c9e012ce15e79221c4206bc06797552d5881098/screenshots/text.png
--------------------------------------------------------------------------------