├── .gitignore ├── README.md ├── args.go ├── deploy_resources ├── file_record.go ├── file_tree └── file_tree.go ├── find.go ├── find_and_replace.go ├── inotify_unix.go ├── inotify_windows.go ├── lang.go ├── main.go ├── menu_callback.go ├── navigation.go ├── options.go ├── resource ├── .tabbyignore ├── classic.xml └── go.lang ├── search_view.go ├── session.go ├── tools.go ├── tree.go └── tree_view.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.6 2 | tabby 3 | *~ 4 | *.bak 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | tabby 2 | ====== 3 | 4 | Source code editor written in Go using go-gtk bindings. It aims to handle 5 | navigation effectively among large number of files. 6 | 7 | screenshot: 8 | ----------- 9 | ![tabby!](https://github.com/mikhailt/tabby/raw/gh-pages/tabby.png "tabby!") 10 | 11 | dependencies: 12 | -------- 13 | go 14 | go-gtk 15 | libgtk2.0-dev 16 | libgtksourceview2.0-dev 17 | 18 | build & run: 19 | -------- 20 | 21 | 0. Install Go & set GOPATH 22 | 1. Put tabby git repo into your $GOPATH/src/ directory 23 | 2. type "go install tabby" 24 | 3. call $GOPATH/bin/tabby 25 | -------------------------------------------------------------------------------- /args.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/mattn/go-gtk/gdk" 5 | "strings" 6 | "os" 7 | "flag" 8 | "net" 9 | "strconv" 10 | "runtime" 11 | ) 12 | 13 | var listener net.Listener 14 | var tabby_args []string 15 | var pfocus_line *int 16 | var pstandalone *bool 17 | 18 | func open_files_from_args() { 19 | for _, s := range tabby_args { 20 | open_file_from_args(prefixed_path(s), *pfocus_line) 21 | } 22 | } 23 | 24 | func tabby_server() { 25 | var focus_line int 26 | buf := make([]byte, 1024) 27 | 28 | for { 29 | c, _ := listener.Accept() 30 | if nil != c { 31 | nread, err := c.Read(buf) 32 | if 0 >= nread { 33 | tabby_log("server: read from unix socket: " + err.Error()) 34 | c.Close() 35 | continue 36 | } 37 | 38 | // At this point buf contains '\n' separated file names preceeded by focus 39 | // line number. Double '\n' at the end of list. 40 | 41 | gdk.ThreadsEnter() 42 | 43 | opened_cnt := 0 44 | s := buf[:] 45 | for cnt := 0; ; cnt++ { 46 | en := strings.Index(string(s), "\n") 47 | if 0 == en { 48 | break 49 | } 50 | if 0 == cnt { 51 | focus_line, _ = strconv.Atoi(string(s[:en])) 52 | } else { 53 | if open_file_from_args(string(s[:en]), focus_line) { 54 | opened_cnt++ 55 | } 56 | } 57 | s = s[en+1:] 58 | } 59 | if opened_cnt > 0 { 60 | main_window.Present() 61 | file_tree_store() 62 | new_file := file_stack_pop() 63 | file_save_current() 64 | file_switch_to(new_file) 65 | } 66 | 67 | gdk.ThreadsLeave() 68 | 69 | c.Close() 70 | } else { 71 | // Dirty hack! There is no way to distinguish two cases: 72 | // 1) Accept returns error because socket was closed on tabby exit. 73 | // 2) Real error occured. 74 | // Commenting this line out to avoid misleading error messages on exit. 75 | //tabby_log(e.String()) 76 | return 77 | } 78 | } 79 | } 80 | 81 | // Returns true if start of real tabby instance required and false o/w. 82 | // In case of false sends arguments to tabby server. 83 | func provide_tabby_server(cnt int) bool { 84 | if cnt > 3 { 85 | return true 86 | } 87 | if *pstandalone { 88 | return true 89 | } 90 | 91 | if runtime.GOOS == "windows" { 92 | return true 93 | } 94 | user := os.Getenv("USER") 95 | socket_name := "/tmp/tabby-" + user 96 | listener, _ = net.Listen("unix", socket_name) 97 | if nil == listener { 98 | // Assume that socket or file with such name already exists. 99 | conn, _ := net.Dial("unix", socket_name) 100 | if nil == conn { 101 | // Socket exists but we cannot connect to it. Delete socket file then 102 | // and repeat the logic. 103 | os.Remove(socket_name) 104 | return provide_tabby_server(cnt + 1) 105 | } 106 | // Dial succeeded. 107 | for y := 0; y < len(tabby_args); y++ { 108 | println(tabby_args[y]) 109 | } 110 | defer conn.Close() 111 | if len(tabby_args) > 0 { 112 | conn.Write([]byte(pack_tabby_args())) 113 | } 114 | return false 115 | } 116 | // Ok, this instance of tabby becomes a server. 117 | go tabby_server() 118 | return true 119 | } 120 | 121 | // Returns true/false whether new instance of tabby editor have to be spawned. 122 | func init_args() bool { 123 | pfocus_line = flag.Int("f", 1, "Focus line") 124 | pstandalone = flag.Bool("s", false, "Forces to open new instance of tabby.") 125 | flag.Parse() 126 | tabby_args = flag.Args() 127 | 128 | return provide_tabby_server(0) 129 | } 130 | 131 | func pack_tabby_args() string { 132 | res := strconv.Itoa(*pfocus_line) + "\n" 133 | for _, s := range tabby_args { 134 | res += prefixed_path(s) + "\n" 135 | } 136 | res += "\n" 137 | return res 138 | } 139 | 140 | func simplified_path(file string) string { 141 | res := file 142 | for { 143 | i := strings.Index(res, "/./") 144 | if -1 == i { 145 | break 146 | } 147 | res = res[:i+1] + res[i+3:] 148 | } 149 | for { 150 | i := strings.Index(res, "/../") 151 | if -1 == i { 152 | break 153 | } 154 | prev_slash := i - 1 155 | for ; '/' != res[prev_slash]; prev_slash-- { 156 | } 157 | res = res[:prev_slash+1] + res[i+4:] 158 | } 159 | return res 160 | } 161 | 162 | func prefixed_path(file string) string { 163 | if '/' != file[0] { 164 | // Relative file name. 165 | wd, err := os.Getwd() 166 | if "" == wd { 167 | tabby_log(err.Error()) 168 | } else { 169 | file = wd + "/" + file 170 | } 171 | } 172 | return file 173 | } 174 | 175 | func open_file_from_args(file string, focus_line int) bool { 176 | split_file := strings.SplitN(file, ":", 2) 177 | if len(split_file) >= 2 { 178 | focus_line, _ = strconv.Atoi(split_file[1]) 179 | } 180 | file = simplified_path(split_file[0]) 181 | if false == session_open_and_read_file(file) { 182 | return false 183 | } 184 | rec, found := file_map[file] 185 | if found { 186 | cur_line := 1 187 | var y int 188 | for y = 0; y < len(rec.buf); y++ { 189 | if cur_line == focus_line { 190 | break 191 | } 192 | if rec.buf[y] == '\n' { 193 | cur_line++ 194 | } 195 | } 196 | rec.sel_be = y 197 | rec.sel_en = y 198 | } else { 199 | return false 200 | } 201 | return true 202 | } 203 | -------------------------------------------------------------------------------- /deploy_resources: -------------------------------------------------------------------------------- 1 | PREFIX=/usr 2 | sudo cp resource/go.lang ${PREFIX}/share/gtksourceview-2.0/language-specs/ 3 | sudo chmod 644 ${PREFIX}/share/gtksourceview-2.0/language-specs/go.lang 4 | sudo cp resource/classic.xml ${PREFIX}/share/gtksourceview-2.0/styles/ 5 | sudo chmod 644 ${PREFIX}/share/gtksourceview-2.0/styles/classic.xml 6 | cp resource/.tabbyignore ~ 7 | -------------------------------------------------------------------------------- /file_record.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type FileRecord struct { 4 | buf []byte 5 | error []byte 6 | modified bool 7 | sel_be int 8 | sel_en int 9 | } 10 | 11 | var file_map map[string]*FileRecord 12 | 13 | func file_opened(name string) bool { 14 | _, found := file_map[name] 15 | return found 16 | } 17 | 18 | func delete_file_record(name string) { 19 | _, found := file_map[name] 20 | if false == found { 21 | return 22 | } 23 | inotify_rm_watch(name) 24 | file_tree_remove(&file_tree_root, name, true) 25 | delete(file_map, name) 26 | } 27 | 28 | func add_file_record(name string, buf []byte, bump_flag bool) bool { 29 | _, found := file_map[name] 30 | if found { 31 | if bump_flag { 32 | bump_message("File " + name + " is already open") 33 | } 34 | return false 35 | } 36 | rec := new(FileRecord) 37 | file_map[name] = rec 38 | rec.modified = false 39 | rec.buf = buf 40 | file_tree_insert(name, rec) 41 | if file_is_saved(name) { 42 | inotify_add_watch(name) 43 | } 44 | return true 45 | } 46 | -------------------------------------------------------------------------------- /file_tree/file_tree.go: -------------------------------------------------------------------------------- 1 | package file_tree 2 | 3 | /* 4 | #include 5 | #include 6 | #include 7 | 8 | void tabby_renderer(GtkTreeViewColumn *col, 9 | GtkCellRenderer *renderer, 10 | GtkTreeModel *model, 11 | GtkTreeIter *iter, 12 | gpointer user_data) { 13 | gchar* str; 14 | unsigned char c; 15 | 16 | gtk_tree_model_get(model, iter, 0, &str, -1); 17 | c = str[0]; 18 | if ('d' == c) { 19 | g_object_set(renderer, "foreground", "Blue", "foreground-set", TRUE, 20 | "background", "White", "background-set", TRUE, NULL); 21 | } else if ('D' == c) { 22 | g_object_set(renderer, "foreground", "Blue", "foreground-set", TRUE, 23 | "background", "#DBEDFF", "background-set", TRUE, NULL); 24 | } else { 25 | if (('C' == c) || ('B' == c)) { 26 | g_object_set(renderer, "background", "#B3FFC2", "background-set", TRUE, NULL); 27 | } else { 28 | g_object_set(renderer, "background", "#FFFAFA", "background-set", TRUE, NULL); 29 | } 30 | if (('c' == c) || ('C' == c)) { 31 | g_object_set(renderer, "foreground", "Red", "foreground-set", TRUE, NULL); 32 | } else { 33 | g_object_set(renderer, "foreground", "Black", "foreground-set", TRUE, NULL); 34 | } 35 | } 36 | g_object_set(renderer, "text", str + 1, NULL); 37 | free(str); 38 | } 39 | 40 | void search_renderer(GtkTreeViewColumn *col, 41 | GtkCellRenderer *renderer, 42 | GtkTreeModel *model, 43 | GtkTreeIter *iter, 44 | gpointer user_data) { 45 | gchar* str; 46 | gchar* p; 47 | 48 | gtk_tree_model_get(model, iter, 0, &str, -1); 49 | for (p = &str[strlen(str) - 1]; '/' != *p; --p) { 50 | ; 51 | } 52 | g_object_set(renderer, "text", p + 1, NULL); 53 | free(str); 54 | } 55 | 56 | static void* create_tabby_file_tree() { 57 | GtkTreeViewColumn *col; 58 | GtkCellRenderer *renderer; 59 | GtkWidget *view; 60 | 61 | view = gtk_tree_view_new(); 62 | col = gtk_tree_view_column_new(); 63 | gtk_tree_view_append_column(GTK_TREE_VIEW(view), col); 64 | renderer = gtk_cell_renderer_text_new(); 65 | gtk_tree_view_column_pack_start(col, renderer, TRUE); 66 | gtk_tree_view_column_set_cell_data_func(col, renderer, tabby_renderer, NULL, 67 | NULL); 68 | return view; 69 | } 70 | 71 | static void* create_tabby_search_tree() { 72 | GtkTreeViewColumn *col; 73 | GtkCellRenderer *renderer; 74 | GtkWidget *view; 75 | 76 | view = gtk_tree_view_new(); 77 | col = gtk_tree_view_column_new(); 78 | gtk_tree_view_append_column(GTK_TREE_VIEW(view), col); 79 | renderer = gtk_cell_renderer_text_new(); 80 | gtk_tree_view_column_pack_start(col, renderer, TRUE); 81 | gtk_tree_view_column_set_cell_data_func(col, renderer, search_renderer, NULL, 82 | NULL); 83 | return view; 84 | } 85 | */ 86 | // #cgo pkg-config: gtk+-2.0 87 | import "C" 88 | import "github.com/mattn/go-gtk/gtk" 89 | 90 | func NewFileTree() *gtk.TreeView { 91 | return >k.TreeView{gtk.Container{ 92 | *gtk.WidgetFromNative(C.create_tabby_file_tree())}} 93 | } 94 | 95 | func NewSearchTree() *gtk.TreeView { 96 | return >k.TreeView{gtk.Container{ 97 | *gtk.WidgetFromNative(C.create_tabby_search_tree())}} 98 | } 99 | -------------------------------------------------------------------------------- /find.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/mattn/go-gtk/gtk" 5 | "github.com/mattn/go-gtk/gdk" 6 | "strings" 7 | ) 8 | 9 | func find_global(pattern string, find_file bool) { 10 | var pos int 11 | if find_file { 12 | prev_pattern = "" 13 | } else { 14 | prev_pattern = pattern 15 | } 16 | search_view.store.Clear() 17 | for name, rec := range file_map { 18 | if find_file { 19 | pos = strings.Index(name, pattern) 20 | } else { 21 | if name == cur_file { 22 | // find_in_current_file does required work for cur_file. 23 | continue 24 | } 25 | pos = strings.Index(string(rec.buf), pattern) 26 | } 27 | if -1 != pos { 28 | search_view.AddFile(name) 29 | } 30 | } 31 | } 32 | 33 | func find_cb() { 34 | find_common(false) 35 | } 36 | 37 | func find_file_cb() { 38 | find_common(true) 39 | } 40 | 41 | func find_common(find_file bool) { 42 | found_in_cur_file := false 43 | dialog_ok, pattern, global, find_file := find_dialog(find_file) 44 | if false == dialog_ok { 45 | return 46 | } 47 | if global { 48 | search_view.PrepareToSearch() 49 | } 50 | if find_file { 51 | find_global(pattern, true) 52 | } else { 53 | if global { 54 | find_global(pattern, false) 55 | } 56 | found_in_cur_file = find_in_current_file(pattern, global) 57 | } 58 | if global && !found_in_cur_file { 59 | search_view.SetCursor(0) 60 | } 61 | } 62 | 63 | // Returns true if pattern was found in current file, false o/w. 64 | func find_in_current_file(pattern string, global bool) bool { 65 | var be, en gtk.TextIter 66 | source_buf.GetSelectionBounds(&be, &en) 67 | if find_next_instance(&en, &be, &en, pattern) { 68 | move_focus_and_selection(&be, &en) 69 | mark_set_cb() 70 | if global { 71 | search_view.AddFile(cur_file) 72 | } 73 | return true 74 | } 75 | return false 76 | } 77 | 78 | func find_dialog(find_file bool) (bool, string, bool, bool) { 79 | dialog := gtk.NewDialog() 80 | defer dialog.Destroy() 81 | dialog.SetTitle("Find") 82 | dialog.AddButton("_Find", gtk.RESPONSE_ACCEPT) 83 | dialog.AddButton("_Cancel", gtk.RESPONSE_CANCEL) 84 | w := dialog.GetWidgetForResponse(int(gtk.RESPONSE_ACCEPT)) 85 | dialog.AddAccelGroup(accel_group) 86 | w.AddAccelerator("clicked", accel_group, gdk.KEY_Return, 87 | 0, gtk.ACCEL_VISIBLE) 88 | entry := find_entry_with_history() 89 | global_button := gtk.NewCheckButtonWithLabel("Global") 90 | global_button.SetVisible(true) 91 | global_button.SetActive(prev_global) 92 | file_button := gtk.NewCheckButtonWithLabel("Find file by name pattern") 93 | file_button.SetVisible(true) 94 | file_button.SetActive(find_file) 95 | vbox := dialog.GetVBox() 96 | vbox.Add(entry) 97 | vbox.Add(global_button) 98 | vbox.Add(file_button) 99 | if gtk.RESPONSE_ACCEPT == dialog.Run() { 100 | entry_text := entry.GetActiveText() 101 | if nil == search_history { 102 | search_history = make([]string, 1) 103 | search_history[0] = entry_text 104 | } else { 105 | be := 0 106 | if 10 <= len(search_history) { 107 | be = 1 108 | } 109 | search_history = append(search_history[be:], entry_text) 110 | } 111 | prev_global = global_button.GetActive() 112 | return true, entry_text, prev_global, file_button.GetActive() 113 | } 114 | return false, "", false, false 115 | } 116 | -------------------------------------------------------------------------------- /find_and_replace.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/mattn/go-gtk/gtk" 5 | "github.com/mattn/go-gtk/gdk" 6 | "strings" 7 | "strconv" 8 | ) 9 | 10 | func fnr_cb() { 11 | fnr_dialog() 12 | } 13 | 14 | func fnr_dialog() { 15 | var fnr_cnt int = 0 16 | var scope_be, scope_en gtk.TextIter 17 | if MAX_SEL_LEN < len(source_selection()) { 18 | source_buf.GetSelectionBounds(&scope_be, &scope_en) 19 | } else { 20 | source_buf.GetStartIter(&scope_be) 21 | source_buf.GetEndIter(&scope_en) 22 | } 23 | source_buf.CreateMark("fnr_be", &scope_be, true) 24 | source_buf.CreateMark("fnr_en", &scope_en, false) 25 | var map_filled bool = false 26 | var global_map map[string]int 27 | var insert_set bool = false 28 | 29 | dialog := gtk.NewDialog() 30 | dialog.SetTitle("Find and Replace") 31 | dialog.AddButton("_Find Next", gtk.RESPONSE_OK) 32 | dialog.AddButton("_Replace", gtk.RESPONSE_YES) 33 | dialog.AddButton("Replace _All", gtk.RESPONSE_APPLY) 34 | dialog.AddButton("_Close", gtk.RESPONSE_CLOSE) 35 | dialog.AddAccelGroup(accel_group) 36 | 37 | entry := find_entry_with_history() 38 | replacement := find_entry_with_history() 39 | 40 | global_button := gtk.NewCheckButtonWithLabel("Global") 41 | global_button.SetVisible(true) 42 | global_button.SetActive(prev_global) 43 | 44 | vbox := dialog.GetVBox() 45 | vbox.Add(entry) 46 | vbox.Add(replacement) 47 | vbox.Add(global_button) 48 | 49 | find_next_button := dialog.GetWidgetForResponse(int(gtk.RESPONSE_OK)) 50 | replace_button := dialog.GetWidgetForResponse(int(gtk.RESPONSE_YES)) 51 | replace_all_button := dialog.GetWidgetForResponse(int(gtk.RESPONSE_APPLY)) 52 | close_button := dialog.GetWidgetForResponse(int(gtk.RESPONSE_CLOSE)) 53 | 54 | find_next_button.Connect("clicked", func() { 55 | fnr_pre_cb(global_button, &insert_set) 56 | if !fnr_find_next(entry.GetActiveText(), prev_global, &map_filled, &global_map) { 57 | fnr_close_and_report(dialog, fnr_cnt) 58 | } 59 | }, 60 | nil) 61 | find_next_button.AddAccelerator("clicked", accel_group, gdk.KEY_Return, 62 | 0, gtk.ACCEL_VISIBLE) 63 | 64 | replace_button.Connect("clicked", func() { 65 | fnr_pre_cb(global_button, &insert_set) 66 | done, next_found := fnr_replace(entry.GetActiveText(), replacement.GetActiveText(), 67 | prev_global, &map_filled, &global_map) 68 | fnr_cnt += done 69 | if !next_found { 70 | fnr_close_and_report(dialog, fnr_cnt) 71 | } 72 | }, 73 | nil) 74 | 75 | replace_all_button.Connect("clicked", func() { 76 | insert_set = false 77 | fnr_pre_cb(global_button, &insert_set) 78 | fnr_cnt += fnr_replace_all_local(entry.GetActiveText(), replacement.GetActiveText()) 79 | if prev_global { 80 | fnr_cnt += fnr_replace_all_global(entry.GetActiveText(), replacement.GetActiveText()) 81 | file_tree_store() 82 | } 83 | fnr_close_and_report(dialog, fnr_cnt) 84 | }, 85 | nil) 86 | 87 | close_button.Connect("clicked", func() { dialog.Destroy() }, nil) 88 | 89 | dialog.Run() 90 | } 91 | 92 | func fnr_replace_all_local(entry string, replacement string) int { 93 | cnt := 0 94 | var t bool = true 95 | if !fnr_find_next(entry, false, &t, nil) { 96 | return 0 97 | } 98 | for { 99 | done, next_found := fnr_replace(entry, replacement, false, &t, nil) 100 | cnt += done 101 | if !next_found { 102 | break 103 | } 104 | } 105 | return cnt 106 | } 107 | 108 | func fnr_replace_all_global(entry, replacement string) int { 109 | total_cnt := 0 110 | lent := len(entry) 111 | lrep := len(replacement) 112 | inds := make(map[int]int) 113 | for file, rec := range file_map { 114 | if file == cur_file { 115 | continue 116 | } 117 | cnt := 0 118 | scope := rec.buf[:] 119 | for { 120 | pos := strings.Index(string(scope), entry) 121 | if -1 == pos { 122 | break 123 | } 124 | inds[cnt] = pos 125 | cnt++ 126 | scope = scope[pos+lent:] 127 | } 128 | if 0 == cnt { 129 | continue 130 | } 131 | buf := make([]byte, len(rec.buf)+cnt*(lrep-lent)) 132 | scope = rec.buf[:] 133 | dest_scope := buf[:] 134 | for y := 0; y < cnt; y++ { 135 | shift := inds[y] 136 | copy(dest_scope, scope[:shift]) 137 | dest_scope = dest_scope[shift:] 138 | copy(dest_scope, replacement) 139 | dest_scope = dest_scope[lrep:] 140 | scope = scope[shift+lent:] 141 | } 142 | copy(dest_scope, scope) 143 | rec.buf = buf 144 | rec.modified = true 145 | total_cnt += cnt 146 | } 147 | return total_cnt 148 | } 149 | 150 | func fnr_pre_cb(global_button *gtk.CheckButton, insert_set *bool) { 151 | prev_global = global_button.GetActive() 152 | fnr_refresh_scope(prev_global) 153 | fnr_set_insert(insert_set) 154 | } 155 | 156 | func fnr_close_and_report(dialog *gtk.Dialog, fnr_cnt int) { 157 | dialog.Destroy() 158 | bump_message(strconv.Itoa(fnr_cnt) + " replacements were done.") 159 | } 160 | 161 | func fnr_set_insert(insert_set *bool) { 162 | if false == *insert_set { 163 | *insert_set = true 164 | var scope_be gtk.TextIter 165 | get_iter_at_mark_by_name("fnr_be", &scope_be) 166 | source_buf.MoveMarkByName("insert", &scope_be) 167 | source_buf.MoveMarkByName("selection_bound", &scope_be) 168 | } 169 | } 170 | 171 | func fnr_refresh_scope(global bool) { 172 | var be, en gtk.TextIter 173 | if global { 174 | source_buf.GetStartIter(&be) 175 | source_buf.GetEndIter(&en) 176 | source_buf.CreateMark("fnr_be", &be, true) 177 | source_buf.CreateMark("fnr_en", &en, false) 178 | } 179 | } 180 | 181 | func fnr_find_next(pattern string, global bool, map_filled *bool, m *map[string]int) bool { 182 | var be, en, scope_en gtk.TextIter 183 | get_iter_at_mark_by_name("fnr_en", &scope_en) 184 | get_iter_at_mark_by_name("selection_bound", &en) 185 | if en.ForwardSearch(pattern, 0, &be, &en, &scope_en) { 186 | move_focus_and_selection(&be, &en) 187 | return true 188 | } 189 | // Have to switch to next file or to beginning of current depending on . 190 | if global { 191 | // Switch to next file. 192 | fnr_find_next_fill_global_map(pattern, m, map_filled) 193 | next_file := pop_string_from_map(m) 194 | if "" == next_file { 195 | return false 196 | } 197 | file_save_current() 198 | file_switch_to(next_file) 199 | fnr_refresh_scope(true) 200 | source_buf.GetStartIter(&be) 201 | source_buf.MoveMarkByName("insert", &be) 202 | source_buf.MoveMarkByName("selection_bound", &be) 203 | return fnr_find_next(pattern, global, map_filled, m) 204 | } else { 205 | // Temporary fix. Is there necessity to search the document all over again? 206 | return false 207 | // Start search from beginning of scope. 208 | // get_iter_at_mark_by_name("fnr_be", &be) 209 | // if be.ForwardSearch(pattern, 0, &be, &en, &scope_en) { 210 | // move_focus_and_selection(&be, &en) 211 | // return true 212 | //} else { 213 | // return false 214 | //} 215 | } 216 | return false 217 | } 218 | 219 | func fnr_find_next_fill_global_map(pattern string, m *map[string]int, map_filled *bool) { 220 | if *map_filled { 221 | return 222 | } 223 | *map_filled = true 224 | *m = make(map[string]int) 225 | for file, rec := range file_map { 226 | if cur_file == file { 227 | continue 228 | } 229 | if -1 != strings.Index(string(rec.buf), pattern) { 230 | (*m)[file] = 1 231 | } 232 | } 233 | } 234 | 235 | 236 | // Returns (done, next_found) 237 | func fnr_replace(entry string, replacement string, global bool, map_filled *bool, global_map *map[string]int) (int, bool) { 238 | if entry != source_selection() { 239 | return 0, true 240 | } 241 | source_buf.DeleteSelection(false, true) 242 | source_buf.InsertAtCursor(replacement) 243 | var be, en gtk.TextIter 244 | source_buf.GetSelectionBounds(&be, &en) 245 | source_buf.MoveMarkByName("insert", &en) 246 | return 1, fnr_find_next(entry, global, map_filled, global_map) 247 | } 248 | 249 | func pop_string_from_map(m *map[string]int) string { 250 | if 0 == len(*m) { 251 | return "" 252 | } 253 | for s, _ := range *m { 254 | delete(*m, s) 255 | return s 256 | } 257 | return "" 258 | } 259 | 260 | func get_iter_at_mark_by_name(mark_name string, iter *gtk.TextIter) { 261 | mark := source_buf.GetMark(mark_name) 262 | source_buf.GetIterAtMark(iter, mark) 263 | } 264 | -------------------------------------------------------------------------------- /inotify_unix.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "syscall" 5 | "unsafe" 6 | "github.com/mattn/go-gtk/gdk" 7 | "github.com/mattn/go-gtk/gtk" 8 | ) 9 | 10 | var name_by_wd map[int32]string 11 | var wd_by_name map[string]int32 12 | 13 | var inotify_fd int 14 | var event_size int 15 | 16 | var epoll_fd int 17 | 18 | const NEVENTS int = 1024 19 | 20 | func init_inotify() { 21 | var err error 22 | 23 | name_by_wd = make(map[int32]string) 24 | wd_by_name = make(map[string]int32) 25 | var event syscall.InotifyEvent 26 | event_size = int(unsafe.Sizeof(event)) 27 | inotify_fd, _ = syscall.InotifyInit() 28 | if -1 == inotify_fd { 29 | bump_message("InotifyInit failed, file changes outside of tabby " + 30 | "will remain unnoticed") 31 | return 32 | } 33 | epoll_fd, err = syscall.EpollCreate(1) 34 | if -1 == epoll_fd { 35 | tabby_log("init_inotify: " + err.Error()) 36 | } 37 | var epoll_event syscall.EpollEvent 38 | epoll_event.Events = syscall.EPOLLIN 39 | syscall.EpollCtl(epoll_fd, syscall.EPOLL_CTL_ADD, inotify_fd, &epoll_event) 40 | go inotify_observe() 41 | } 42 | 43 | func inotify_add_watch(name string) { 44 | wd, err := syscall.InotifyAddWatch(inotify_fd, name, 45 | syscall.IN_MODIFY|syscall.IN_DELETE_SELF|syscall.IN_MOVE_SELF) 46 | if -1 == wd { 47 | if err == syscall.ENOENT { 48 | // Dirty hack. 49 | return 50 | } 51 | tabby_log("InotifyAddWatch failed, changes of file " + name + 52 | " outside of tabby will remain unnoticed, errno = " + err.Error()) 53 | return 54 | } 55 | name_by_wd[int32(wd)] = name 56 | wd_by_name[name] = int32(wd) 57 | } 58 | 59 | func inotify_rm_watch(name string) { 60 | wd, found := wd_by_name[name] 61 | if false == found { 62 | return 63 | } 64 | retval, _ /*err*/ := syscall.InotifyRmWatch(inotify_fd, uint32(wd)) 65 | if -1 == retval { 66 | //println("tabby: InotifyRmWatch failed, errno = ", err) 67 | return 68 | } 69 | delete(name_by_wd, wd) 70 | delete(wd_by_name, name) 71 | } 72 | 73 | func inotify_observe() { 74 | buf := make([]byte, event_size*NEVENTS) 75 | for { 76 | collect := inotify_observe_collect(buf) 77 | if 0 == len(collect) { 78 | continue 79 | } 80 | gdk.ThreadsEnter() 81 | file_save_current() 82 | reload := inotify_dialog(collect) 83 | for name, _ := range collect { 84 | rec, rec_found := file_map[name] 85 | if false == rec_found { 86 | tabby_log("inotify_observe: " + name + " not found in file_map") 87 | continue 88 | } 89 | if reload { 90 | // Reload file content. 91 | read_ok, buf := open_file_read_to_buf(name, true) 92 | if read_ok { 93 | rec.buf = buf 94 | rec.modified = false 95 | inotify_rm_watch(name) 96 | inotify_add_watch(name) 97 | } else { 98 | rec.modified = true 99 | } 100 | } else { 101 | // Keep file as is. 102 | rec.modified = true 103 | } 104 | } 105 | file_tree_store() 106 | // So as to renew current TextBuffer it is required to switch to cur_file. 107 | file_switch_to(cur_file) 108 | gdk.ThreadsLeave() 109 | } 110 | } 111 | 112 | func inotify_observe_collect(buf []byte) map[string]int { 113 | epoll_buf := make([]syscall.EpollEvent, 1) 114 | collect := make(map[string]int) 115 | for { 116 | nread, _ := syscall.Read(inotify_fd, buf) 117 | for offset := 0; offset < nread; offset += event_size { 118 | event := (*syscall.InotifyEvent)(unsafe.Pointer(&buf[offset])) 119 | if syscall.IN_IGNORED == event.Mask { 120 | continue 121 | } 122 | collect[name_by_wd[event.Wd]] = 1 123 | } 124 | nevents, err := syscall.EpollWait(epoll_fd, epoll_buf, 500) 125 | if 0 >= nevents { 126 | if -1 == nevents { 127 | tabby_log("inotify_observe_collect: " + err.Error()) 128 | } 129 | break 130 | } 131 | } 132 | return collect 133 | } 134 | 135 | // Returns true in case of reloading files, and false in case of keeping as is. 136 | func inotify_dialog(s map[string]int) bool { 137 | if nil == accel_group { 138 | accel_group = gtk.NewAccelGroup() 139 | } 140 | inotify_dlg := gtk.NewDialog() 141 | defer inotify_dlg.Destroy() 142 | inotify_dlg.SetTitle("Some files have beed modified outside of tabby") 143 | inotify_dlg.AddButton("_Reload all", gtk.RESPONSE_ACCEPT) 144 | inotify_dlg.AddButton("_Keep all as is", gtk.RESPONSE_CANCEL) 145 | w := inotify_dlg.GetWidgetForResponse(int(gtk.RESPONSE_ACCEPT)) 146 | inotify_dlg.AddAccelGroup(accel_group) 147 | w.AddAccelerator("clicked", accel_group, gdk.KEY_Return, 148 | 0, gtk.ACCEL_VISIBLE) 149 | inotify_dlg.SetSizeRequest(800, 350) 150 | inotify_store := gtk.NewTreeStore(gtk.TYPE_STRING) 151 | inotify_view := gtk.NewTreeView() 152 | inotify_view.AppendColumn( 153 | gtk.NewTreeViewColumnWithAttributes("text", gtk.NewCellRendererText(), "text", 0)) 154 | inotify_view.ModifyFontEasy("Regular 8") 155 | inotify_model := inotify_store.ToTreeModel() 156 | inotify_view.SetModel(inotify_model) 157 | inotify_view.SetHeadersVisible(false) 158 | var iter gtk.TreeIter 159 | for name, _ := range s { 160 | inotify_store.Append(&iter, nil) 161 | inotify_store.Set(&iter, name) 162 | } 163 | inotify_view.SetVisible(true) 164 | view_window := gtk.NewScrolledWindow(nil, nil) 165 | view_window.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) 166 | view_window.SetVisible(true) 167 | view_window.Add(inotify_view) 168 | vbox := inotify_dlg.GetVBox() 169 | vbox.Add(view_window) 170 | if gtk.RESPONSE_ACCEPT == inotify_dlg.Run() { 171 | return true 172 | } 173 | return false 174 | } 175 | -------------------------------------------------------------------------------- /inotify_windows.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func init_inotify() { 4 | } 5 | 6 | func inotify_rm_watch(name string) { 7 | } 8 | 9 | func inotify_observe() { 10 | } 11 | 12 | func inotify_observe_collect(buf []byte) map[string]int { 13 | return make(map[string]int) 14 | } 15 | 16 | func inotify_dialog(s map[string]int) bool { 17 | return false 18 | } 19 | 20 | func inotify_add_watch(name string) { 21 | } 22 | -------------------------------------------------------------------------------- /lang.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/mattn/go-gtk/gtksourceview" 5 | ) 6 | 7 | var lang_map map[string]*gtksourceview.SourceLanguage 8 | 9 | var prev_lang string = "default" 10 | 11 | func lang_refresh() { 12 | ext := lang_get_extension(cur_file) 13 | lang, found := lang_map[ext] 14 | if !found { 15 | lang = lang_map["default"] 16 | ext = "default" 17 | } 18 | if ext == prev_lang { 19 | return 20 | } 21 | prev_lang = ext 22 | source_buf.SetLanguage(lang) 23 | } 24 | 25 | func lang_get_extension(name string) string { 26 | for y := len(name) - 1; y >= 0; y-- { 27 | if ('.' == name[y]) || ('/' == name[y]) { 28 | return name[y+1:] 29 | } 30 | } 31 | return "" 32 | } 33 | 34 | func init_lang() { 35 | lang_man := gtksourceview.SourceLanguageManagerGetDefault() 36 | lang_map = make(map[string]*gtksourceview.SourceLanguage) 37 | lang_map["go"] = lang_man.GetLanguage("go") 38 | lang_map["c"] = lang_man.GetLanguage("c") 39 | lang_map["h"] = lang_man.GetLanguage("cpp") 40 | lang_map["sh"] = lang_man.GetLanguage("sh") 41 | lang_map["diff"] = lang_man.GetLanguage("diff") 42 | lang_map["patch"] = lang_man.GetLanguage("diff") 43 | lang_map["cpp"] = lang_man.GetLanguage("cpp") 44 | lang_map["hpp"] = lang_man.GetLanguage("cpp") 45 | lang_map["cc"] = lang_man.GetLanguage("cpp") 46 | lang_map["latex"] = lang_man.GetLanguage("latex") 47 | lang_map["tex"] = lang_man.GetLanguage("latex") 48 | lang_map["Makefile"] = lang_man.GetLanguage("makefile") 49 | lang_map["am"] = lang_man.GetLanguage("makefile") 50 | lang_map["in"] = lang_man.GetLanguage("makefile") 51 | lang_map["xml"] = lang_man.GetLanguage("xml") 52 | lang_map["py"] = lang_man.GetLanguage("python") 53 | lang_map["scons"] = lang_man.GetLanguage("python") 54 | lang_map["SConstruct"] = lang_man.GetLanguage("python") 55 | lang_map["java"] = lang_man.GetLanguage("java") 56 | 57 | lang_map["default"] = lang_man.GetLanguage("sh") 58 | 59 | source_buf.SetLanguage(lang_map["default"]) 60 | } 61 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/mattn/go-gtk/gtk" 5 | "github.com/mattn/go-gtk/gdk" 6 | "github.com/mattn/go-gtk/glib" 7 | "github.com/mattn/go-gtk/gtksourceview" 8 | "github.com/mikhailt/tabby/file_tree" 9 | "strconv" 10 | "fmt" 11 | ) 12 | 13 | var main_window *gtk.Window 14 | var source_buf *gtksourceview.SourceBuffer 15 | var source_view *gtksourceview.SourceView 16 | 17 | var error_buf *gtk.TextBuffer 18 | var error_view *gtk.TextView 19 | var error_window *gtk.ScrolledWindow 20 | 21 | var tree_view *gtk.TreeView 22 | var tree_store *gtk.TreeStore 23 | var tree_model *gtk.TreeModel 24 | 25 | var search_view SearchView 26 | 27 | var cur_file string = "" 28 | var cur_iter gtk.TreeIter 29 | 30 | func refresh_title() { 31 | if "" == cur_file { 32 | main_window.SetTitle("*") 33 | return 34 | } 35 | var icon byte 36 | if source_buf.GetModified() { 37 | main_window.SetTitle("* " + cur_file) 38 | icon = 'C' 39 | } else { 40 | main_window.SetTitle(cur_file) 41 | icon = 'B' 42 | } 43 | var val glib.GValue 44 | tree_model.GetValue(&cur_iter, 0, &val) 45 | tree_store.Set(&cur_iter, string(icon)+val.GetString()[1:]) 46 | } 47 | 48 | func buf_changed_cb() { 49 | refresh_title() 50 | } 51 | 52 | func bump_message(m string) { 53 | dialog := gtk.NewMessageDialog( 54 | main_window.GetTopLevelAsWindow(), 55 | gtk.DIALOG_MODAL, 56 | gtk.MESSAGE_INFO, 57 | gtk.BUTTONS_OK, 58 | m) 59 | dialog.Run() 60 | dialog.Destroy() 61 | } 62 | 63 | func bump_question(m string) (b bool) { 64 | dialog := gtk.NewMessageDialog( 65 | main_window.GetTopLevelAsWindow(), 66 | gtk.DIALOG_MODAL, 67 | gtk.MESSAGE_WARNING, 68 | gtk.BUTTONS_YES_NO, 69 | m) 70 | b = dialog.Run() == gtk.RESPONSE_YES 71 | dialog.Destroy() 72 | return 73 | } 74 | 75 | func init_tabby() { 76 | init_navigation() 77 | init_inotify() 78 | 79 | search_view.Init() 80 | 81 | source_buf = gtksourceview.NewSourceBuffer() 82 | source_buf.Connect("paste-done", paste_done_cb, nil) 83 | source_buf.Connect("mark-set", mark_set_cb, nil) 84 | source_buf.Connect("modified-changed", buf_changed_cb, nil) 85 | 86 | init_lang() 87 | 88 | source_buf.CreateTag("instance", map[string]string{"background": "#FF8080"}) 89 | 90 | tree_store = gtk.NewTreeStore(gtk.TYPE_STRING) 91 | tree_view = file_tree.NewFileTree() 92 | tree_view.ModifyFontEasy("Regular 8") 93 | tree_model = tree_store.ToTreeModel() 94 | tree_view.SetModel(tree_model) 95 | tree_view.SetHeadersVisible(false) 96 | tree_view.Connect("cursor-changed", tree_view_select_cb, nil) 97 | 98 | error_view = gtk.NewTextView() 99 | error_view.ModifyFontEasy("Monospace Regular 8") 100 | error_view.SetEditable(false) 101 | error_buf = error_view.GetBuffer() 102 | 103 | source_view = gtksourceview.NewSourceViewWithBuffer(source_buf) 104 | source_view.ModifyFontEasy("Monospace Regular 10") 105 | source_view.SetAutoIndent(true) 106 | source_view.SetHighlightCurrentLine(true) 107 | source_view.SetShowLineNumbers(true) 108 | source_view.SetRightMarginPosition(80) 109 | source_view.SetShowRightMargin(true) 110 | source_view.SetIndentWidth(2) 111 | source_view.SetTabWidth(2) 112 | source_view.SetInsertSpacesInsteadOfTabs(opt.space_not_tab) 113 | source_view.SetDrawSpaces(gtksourceview.SOURCE_DRAW_SPACES_TAB) 114 | source_view.SetSmartHomeEnd(gtksourceview.SOURCE_SMART_HOME_END_ALWAYS) 115 | source_view.SetWrapMode(gtk.WRAP_WORD) 116 | 117 | vbox := gtk.NewVBox(false, 0) 118 | inner_hpaned := gtk.NewHPaned() 119 | view_vpaned := gtk.NewVPaned() 120 | outer_hpaned := gtk.NewHPaned() 121 | outer_hpaned.Add1(inner_hpaned) 122 | inner_hpaned.Add2(view_vpaned) 123 | 124 | menubar := gtk.NewMenuBar() 125 | vbox.PackStart(menubar, false, false, 0) 126 | vbox.PackStart(outer_hpaned, true, true, 0) 127 | 128 | file_item := gtk.NewMenuItemWithMnemonic("_File") 129 | menubar.Append(file_item) 130 | file_submenu := gtk.NewMenu() 131 | file_item.SetSubmenu(file_submenu) 132 | 133 | accel_group := gtk.NewAccelGroup() 134 | 135 | new_item := gtk.NewMenuItemWithMnemonic("_New") 136 | file_submenu.Append(new_item) 137 | new_item.Connect("activate", new_cb, nil) 138 | new_item.AddAccelerator("activate", accel_group, gdk.KEY_n, gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) 139 | 140 | open_item := gtk.NewMenuItemWithMnemonic("_Open") 141 | file_submenu.Append(open_item) 142 | open_item.Connect("activate", open_cb, nil) 143 | open_item.AddAccelerator("activate", accel_group, gdk.KEY_o, gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) 144 | 145 | open_rec_item := gtk.NewMenuItemWithMnemonic("Open _Recursively") 146 | file_submenu.Append(open_rec_item) 147 | open_rec_item.Connect("activate", open_rec_cb, nil) 148 | 149 | save_item := gtk.NewMenuItemWithMnemonic("_Save") 150 | file_submenu.Append(save_item) 151 | save_item.Connect("activate", save_cb, nil) 152 | save_item.AddAccelerator("activate", accel_group, gdk.KEY_s, gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) 153 | 154 | save_as_item := gtk.NewMenuItemWithMnemonic("Save _as") 155 | file_submenu.Append(save_as_item) 156 | save_as_item.Connect("activate", save_as_cb, nil) 157 | 158 | close_item := gtk.NewMenuItemWithMnemonic("_Close") 159 | file_submenu.Append(close_item) 160 | close_item.Connect("activate", close_cb, nil) 161 | close_item.AddAccelerator("activate", accel_group, gdk.KEY_w, gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) 162 | 163 | exit_item := gtk.NewMenuItemWithMnemonic("E_xit") 164 | file_submenu.Append(exit_item) 165 | exit_item.Connect("activate", exit_cb, nil) 166 | 167 | navigation_item := gtk.NewMenuItemWithMnemonic("_Navigation") 168 | menubar.Append(navigation_item) 169 | navigation_submenu := gtk.NewMenu() 170 | navigation_item.SetSubmenu(navigation_submenu) 171 | 172 | prev_instance_item := gtk.NewMenuItemWithMnemonic("_Previous Instance") 173 | navigation_submenu.Append(prev_instance_item) 174 | prev_instance_item.Connect("activate", prev_instance_cb, nil) 175 | prev_instance_item.AddAccelerator("activate", accel_group, gdk.KEY_F2, 176 | 0, gtk.ACCEL_VISIBLE) 177 | 178 | next_instance_item := gtk.NewMenuItemWithMnemonic("_Next Instance") 179 | navigation_submenu.Append(next_instance_item) 180 | next_instance_item.Connect("activate", next_instance_cb, nil) 181 | next_instance_item.AddAccelerator("activate", accel_group, gdk.KEY_F3, 182 | 0, gtk.ACCEL_VISIBLE) 183 | 184 | prev_result_item := gtk.NewMenuItemWithMnemonic("Prev search result") 185 | navigation_submenu.Append(prev_result_item) 186 | prev_result_item.Connect("activate", func() {search_view.PrevResult()}, nil) 187 | prev_result_item.AddAccelerator("activate", accel_group, gdk.KEY_F4, 188 | 0, gtk.ACCEL_VISIBLE) 189 | 190 | next_result_item := gtk.NewMenuItemWithMnemonic("Next search result") 191 | navigation_submenu.Append(next_result_item) 192 | next_result_item.Connect("activate", func() {search_view.NextResult()}, nil) 193 | next_result_item.AddAccelerator("activate", accel_group, gdk.KEY_F5, 194 | 0, gtk.ACCEL_VISIBLE) 195 | 196 | find_item := gtk.NewMenuItemWithMnemonic("_Find") 197 | navigation_submenu.Append(find_item) 198 | find_item.Connect("activate", find_cb, nil) 199 | find_item.AddAccelerator("activate", accel_group, gdk.KEY_f, gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) 200 | 201 | find_file_item := gtk.NewMenuItemWithMnemonic("_Find file") 202 | navigation_submenu.Append(find_file_item) 203 | find_file_item.Connect("activate", find_file_cb, nil) 204 | find_file_item.AddAccelerator("activate", accel_group, gdk.KEY_d, gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) 205 | 206 | fnr_item := gtk.NewMenuItemWithMnemonic("Find and Replace") 207 | navigation_submenu.Append(fnr_item) 208 | fnr_item.Connect("activate", fnr_cb, nil) 209 | fnr_item.AddAccelerator("activate", accel_group, gdk.KEY_r, gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) 210 | 211 | prev_file_item := gtk.NewMenuItemWithMnemonic("Prev File") 212 | navigation_submenu.Append(prev_file_item) 213 | prev_file_item.Connect("activate", prev_file_cb, nil) 214 | prev_file_item.AddAccelerator("activate", accel_group, gdk.KEY_F7, 215 | 0, gtk.ACCEL_VISIBLE) 216 | 217 | next_file_item := gtk.NewMenuItemWithMnemonic("Next File") 218 | navigation_submenu.Append(next_file_item) 219 | next_file_item.Connect("activate", next_file_cb, nil) 220 | next_file_item.AddAccelerator("activate", accel_group, gdk.KEY_F8, 221 | 0, gtk.ACCEL_VISIBLE) 222 | 223 | tools_item := gtk.NewMenuItemWithMnemonic("_Tools") 224 | menubar.Append(tools_item) 225 | tools_submenu := gtk.NewMenu() 226 | tools_item.SetSubmenu(tools_submenu) 227 | 228 | gofmt_item := gtk.NewMenuItemWithMnemonic("_Gofmt") 229 | tools_submenu.Append(gofmt_item) 230 | gofmt_item.Connect("activate", gofmt_cb, nil) 231 | gofmt_item.AddAccelerator("activate", accel_group, gdk.KEY_F9, 232 | 0, gtk.ACCEL_VISIBLE) 233 | 234 | gofmtAll_item := gtk.NewMenuItemWithMnemonic("Gofmt _All") 235 | tools_submenu.Append(gofmtAll_item) 236 | gofmtAll_item.Connect("activate", gofmt_all, nil) 237 | gofmtAll_item.AddAccelerator("activate", accel_group, gdk.KEY_F9, gdk.CONTROL_MASK, gtk.ACCEL_VISIBLE) 238 | 239 | options_item := gtk.NewMenuItemWithMnemonic("_Options") 240 | menubar.Append(options_item) 241 | options_submenu := gtk.NewMenu() 242 | options_item.SetSubmenu(options_submenu) 243 | 244 | search_chkitem := gtk.NewCheckMenuItemWithMnemonic("Show _Searchview") 245 | options_submenu.Append(search_chkitem) 246 | search_chkitem.SetActive(opt.show_search) 247 | search_chkitem.Connect("toggled", func() { search_chk_cb(search_chkitem.GetActive()) }, nil) 248 | 249 | error_chkitem := gtk.NewCheckMenuItemWithMnemonic("Show _Errorview") 250 | options_submenu.Append(error_chkitem) 251 | error_chkitem.SetActive(opt.show_error) 252 | error_chkitem.Connect("toggled", func() { error_chk_cb(error_chkitem.GetActive()) }, nil) 253 | 254 | notab_chkitem := gtk.NewCheckMenuItemWithMnemonic("Spaces for _Tabs") 255 | options_submenu.Append(notab_chkitem) 256 | notab_chkitem.SetActive(opt.space_not_tab) 257 | notab_chkitem.Connect("toggled", func() { notab_chk_cb(notab_chkitem.GetActive()) }, nil) 258 | 259 | font_item := gtk.NewMenuItemWithMnemonic("_Font") 260 | options_submenu.Append(font_item) 261 | font_item.Connect("activate", font_cb, nil) 262 | 263 | tabsize_item := gtk.NewMenuItemWithMnemonic("_Tab size") 264 | options_submenu.Append(tabsize_item) 265 | tabsize_submenu := gtk.NewMenu() 266 | tabsize_item.SetSubmenu(tabsize_submenu) 267 | const tabsize_cnt = 8 268 | tabsize_chk := make([]*gtk.CheckMenuItem, tabsize_cnt) 269 | for y := 0; y < tabsize_cnt; y++ { 270 | tabsize_chk[y] = gtk.NewCheckMenuItemWithMnemonic(strconv.Itoa(y + 1)) 271 | tabsize_submenu.Append(tabsize_chk[y]) 272 | cur_ind := y 273 | tabsize_chk[y].Connect("activate", func() { 274 | if false == tabsize_chk[cur_ind].GetActive() { 275 | active_cnt := 0 276 | for j := 0; j < tabsize_cnt; j++ { 277 | if tabsize_chk[j].GetActive() { 278 | active_cnt++ 279 | } 280 | } 281 | if 0 == active_cnt { 282 | tabsize_chk[cur_ind].SetActive(true) 283 | } 284 | return 285 | } 286 | for j := 0; j < tabsize_cnt; j++ { 287 | if j != cur_ind { 288 | tabsize_chk[j].SetActive(false) 289 | } 290 | } 291 | options_set_tabsize(cur_ind + 1) 292 | }, 293 | nil) 294 | } 295 | 296 | tree_window := gtk.NewScrolledWindow(nil, nil) 297 | tree_window.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) 298 | inner_hpaned.Add1(tree_window) 299 | tree_window.Add(tree_view) 300 | 301 | outer_hpaned.Add2(search_view.window) 302 | 303 | text_window := gtk.NewScrolledWindow(nil, nil) 304 | text_window.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) 305 | view_vpaned.Add1(text_window) 306 | text_window.Add(source_view) 307 | 308 | error_window = gtk.NewScrolledWindow(nil, nil) 309 | error_window.SetPolicy(gtk.POLICY_AUTOMATIC, gtk.POLICY_ALWAYS) 310 | view_vpaned.Add2(error_window) 311 | error_window.Add(error_view) 312 | 313 | inner_hpaned.Connect("size_request", func() { ohp_cb(outer_hpaned.GetPosition()) }, nil) 314 | view_vpaned.Connect("size_request", func() { ihp_cb(inner_hpaned.GetPosition()) }, nil) 315 | source_view.Connect("size_request", func() { vvp_cb(view_vpaned.GetPosition()) }, nil) 316 | outer_hpaned.SetPosition(opt.ohp_position) 317 | inner_hpaned.SetPosition(opt.ihp_position) 318 | view_vpaned.SetPosition(opt.vvp_position) 319 | 320 | main_window = gtk.NewWindow(gtk.WINDOW_TOPLEVEL) 321 | main_window.AddAccelGroup(accel_group) 322 | main_window.SetSizeRequest(400, 200) //minimum size 323 | main_window.Resize(opt.window_width, opt.window_height) 324 | main_window.Move(opt.window_x, opt.window_y) 325 | main_window.Connect("destroy", exit_cb, "") 326 | main_window.Connect("configure-event", window_event_cb, "") 327 | main_window.Add(vbox) 328 | // init_tabby blocks for some reason when is called after ShowAll. 329 | init_vars() 330 | main_window.ShowAll() 331 | error_window.SetVisible(opt.show_error) 332 | // Cannot be called before ShowAll. This is also not clear. 333 | file_switch_to(file_stack_pop()) 334 | stack_prev(&file_stack_max) 335 | if "" == cur_file { 336 | new_cb() 337 | } 338 | source_view.GrabFocus() 339 | } 340 | 341 | func tabby_log(m string) { 342 | fmt.Println("tabby: " + m) 343 | } 344 | 345 | func init_vars() { 346 | file_map = make(map[string]*FileRecord) 347 | refresh_title() 348 | if 0 == len(tabby_args) { 349 | session_restore() 350 | } 351 | open_files_from_args() 352 | file_tree_store() 353 | } 354 | 355 | func main() { 356 | gdk.ThreadsInit() 357 | if false == init_args() { 358 | return 359 | } 360 | load_options() 361 | defer save_options() 362 | 363 | gdk.ThreadsEnter() 364 | gtk.Init(nil) 365 | init_tabby() 366 | gtk.Main() 367 | } 368 | -------------------------------------------------------------------------------- /menu_callback.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/mattn/go-gtk/glib" 5 | "github.com/mattn/go-gtk/gtk" 6 | "os" 7 | "strconv" 8 | ) 9 | 10 | var prev_dir string 11 | var last_unsaved int = -1 12 | 13 | func new_cb() { 14 | file_save_current() 15 | last_unsaved++ 16 | file := "unsaved file " + strconv.Itoa(last_unsaved) 17 | add_file_record(file, []byte(""), true) 18 | file_map[file].modified = true 19 | file_tree_store() 20 | file_switch_to(file) 21 | tree_view_set_cur_iter(true) 22 | } 23 | 24 | func open_cb() { 25 | file_save_current() 26 | dialog_ok, dialog_file := file_chooser_dialog(OPEN_DIALOG) 27 | if false == dialog_ok { 28 | return 29 | } 30 | read_ok, buf := open_file_read_to_buf(dialog_file, true) 31 | if false == read_ok { 32 | return 33 | } 34 | if add_file_record(dialog_file, buf, true) { 35 | file_tree_store() 36 | file_switch_to(dialog_file) 37 | } 38 | } 39 | 40 | func open_rec_cb() { 41 | dialog_ok, dialog_dir := file_chooser_dialog(OPEN_DIR_DIALOG) 42 | if false == dialog_ok { 43 | return 44 | } 45 | dir, _ := os.OpenFile(dialog_dir, os.O_RDONLY, 0) 46 | if nil == dir { 47 | bump_message("Unable to open directory " + dialog_dir) 48 | } 49 | open_dir(dir, dialog_dir, true) 50 | dir.Close() 51 | file_tree_store() 52 | } 53 | 54 | func save_cb() { 55 | if !file_is_saved(cur_file) { 56 | save_as_cb() 57 | return 58 | } 59 | inotify_rm_watch(cur_file) 60 | defer inotify_add_watch(cur_file) 61 | file, _ := os.OpenFile(cur_file, os.O_CREATE|os.O_WRONLY, 0644) 62 | if nil == file { 63 | bump_message("Unable to open file for writing: " + cur_file) 64 | return 65 | } 66 | file_save_current() 67 | rec, _ := file_map[cur_file] 68 | nbytes, err := file.WriteString(string(rec.buf)) 69 | if nbytes != len(rec.buf) { 70 | bump_message("Error while writing to file: " + cur_file) 71 | println("nbytes = ", nbytes, " errno = ", err) 72 | return 73 | } 74 | file.Truncate(int64(nbytes)) 75 | file.Close() 76 | 77 | source_buf.SetModified(false) 78 | refresh_title() 79 | } 80 | 81 | func save_as_cb() { 82 | dialog_ok, dialog_file := file_chooser_dialog(SAVE_DIALOG) 83 | if false == dialog_ok { 84 | return 85 | } 86 | var be, en gtk.TextIter 87 | source_buf.GetStartIter(&be) 88 | source_buf.GetEndIter(&en) 89 | text_to_save := source_buf.GetText(&be, &en, true) 90 | add_file_record(dialog_file, []byte(text_to_save), true) 91 | file_tree_store() 92 | file_to_delete := cur_file 93 | file_switch_to(dialog_file) 94 | delete_file_record(file_to_delete) 95 | file_tree_store() 96 | save_cb() 97 | tree_view_set_cur_iter(true) 98 | } 99 | 100 | func exit_cb() { 101 | // Are-you-sure-you-want-to-exit-because-file-is-unsaved logic will be here. 102 | session_save() 103 | if nil != listener { 104 | listener.Close() 105 | } 106 | gtk.MainQuit() 107 | } 108 | 109 | func close_cb() { 110 | if "" == cur_file { 111 | return 112 | } 113 | close_it := !source_buf.GetModified() 114 | if !close_it { 115 | close_it = bump_question("This file has been modified. Close it?") 116 | } 117 | if close_it { 118 | delete_file_record(cur_file) 119 | cur_file = file_stack_pop() 120 | if 0 == len(file_map) { 121 | new_cb() 122 | } 123 | if "" == cur_file { 124 | // Choose random open file then. Previous if implies that there are some 125 | // opened files. At least unsaved. 126 | for cur_file, _ = range file_map { 127 | break 128 | } 129 | } 130 | file_switch_to(cur_file) 131 | file_tree_store() 132 | } 133 | } 134 | 135 | func paste_done_cb() { 136 | var be, en gtk.TextIter 137 | source_buf.GetStartIter(&be) 138 | source_buf.GetEndIter(&en) 139 | source_buf.RemoveTagByName("instance", &be, &en) 140 | selection_flag = false 141 | } 142 | 143 | // Reads file content to newly allocated buffer. 144 | func open_file_read_to_buf(name string, verbose bool) (bool, []byte) { 145 | file, _ := os.OpenFile(name, os.O_RDONLY, 0644) 146 | if nil == file { 147 | bump_message("Unable to open file for reading: " + name) 148 | return false, nil 149 | } 150 | defer file.Close() 151 | stat, _ := file.Stat() 152 | if nil == stat { 153 | bump_message("Unable to stat file: " + name) 154 | return false, nil 155 | } 156 | buf := make([]byte, stat.Size()) 157 | nread, _ := file.Read(buf) 158 | if nread != int(stat.Size()) { 159 | bump_message("Unable to read whole file: " + name) 160 | return false, nil 161 | } 162 | if nread > 0 { 163 | if false == glib.Utf8Validate(buf, nread, nil) { 164 | if verbose { 165 | bump_message("File " + name + " is not correct utf8 text") 166 | } 167 | return false, nil 168 | } 169 | } 170 | return true, buf 171 | } 172 | 173 | func open_dir(dir *os.File, dir_name string, recursively bool) { 174 | names, _ := dir.Readdirnames(-1) 175 | for _, name := range names { 176 | abs_name := dir_name + "/" + name 177 | if name_is_ignored(abs_name) { 178 | continue 179 | } 180 | fi, _ := os.Lstat(abs_name) 181 | if nil == fi { 182 | continue 183 | } 184 | if fi.IsDir() { 185 | if recursively { 186 | child_dir, _ := os.OpenFile(abs_name, os.O_RDONLY, 0) 187 | if nil != child_dir { 188 | open_dir(child_dir, abs_name, true) 189 | } 190 | child_dir.Close() 191 | } 192 | } else { 193 | session_open_and_read_file(abs_name) 194 | } 195 | } 196 | } 197 | 198 | const ( 199 | OPEN_DIALOG = 0 200 | SAVE_DIALOG = 1 201 | OPEN_DIR_DIALOG = 2 202 | ) 203 | 204 | func file_chooser_dialog(t int) (bool, string) { 205 | var action gtk.FileChooserAction 206 | var ok_stock string 207 | if OPEN_DIALOG == t { 208 | action = gtk.FILE_CHOOSER_ACTION_OPEN 209 | ok_stock = gtk.STOCK_OPEN 210 | } else if SAVE_DIALOG == t { 211 | action = gtk.FILE_CHOOSER_ACTION_SAVE 212 | ok_stock = gtk.STOCK_SAVE 213 | } else if OPEN_DIR_DIALOG == t { 214 | action = gtk.FILE_CHOOSER_ACTION_SELECT_FOLDER 215 | ok_stock = gtk.STOCK_OPEN 216 | } 217 | dialog := gtk.NewFileChooserDialog("", source_view.GetTopLevelAsWindow(), 218 | action, 219 | gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL, 220 | ok_stock, gtk.RESPONSE_ACCEPT) 221 | dialog.SetCurrentFolder(prev_dir) 222 | res := dialog.Run() 223 | dialog_folder := dialog.GetCurrentFolder() 224 | dialog_file := dialog.GetFilename() 225 | dialog.Destroy() 226 | if gtk.RESPONSE_ACCEPT == res { 227 | prev_dir = dialog_folder 228 | return true, dialog_file 229 | } 230 | return false, "" 231 | } 232 | 233 | func error_chk_cb(current bool) { 234 | error_window.SetVisible(current) 235 | opt.show_error = current 236 | } 237 | 238 | func search_chk_cb(current bool) { 239 | search_view.window.SetVisible(current) 240 | opt.show_search = current 241 | } 242 | 243 | func notab_chk_cb(current bool) { 244 | opt.space_not_tab = current 245 | source_view.SetInsertSpacesInsteadOfTabs(opt.space_not_tab) 246 | } 247 | 248 | func gofmt_cb() { 249 | gofmt(cur_file) 250 | } 251 | 252 | func font_cb() { 253 | dialog := gtk.NewFontSelectionDialog("Pick a font") 254 | dialog.SetFontName(opt.font) 255 | if gtk.RESPONSE_OK == dialog.Run() { 256 | opt.font = dialog.GetFontName() 257 | source_view.ModifyFontEasy(opt.font) 258 | } 259 | dialog.Destroy() 260 | } 261 | -------------------------------------------------------------------------------- /navigation.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/mattn/go-gtk/gtk" 5 | // "github.com/mattn/go-gtk/gdk" 6 | "runtime" 7 | ) 8 | 9 | const STACK_SIZE = 64 10 | const MAX_SEL_LEN = 128 11 | 12 | var selection_flag bool 13 | var prev_selection string 14 | var search_history []string 15 | 16 | var file_stack [STACK_SIZE]string 17 | var file_stack_top = 0 18 | var file_stack_base = 0 19 | var file_stack_max = 0 20 | 21 | var prev_global bool = true 22 | var prev_pattern string = "" 23 | 24 | var accel_group *gtk.AccelGroup = nil 25 | 26 | func find_entry_with_history() *gtk.ComboBoxEntry { 27 | entry := gtk.NewComboBoxEntryNewText() 28 | entry.SetVisible(true) 29 | selection := source_selection() 30 | if ("" != selection) && (len(selection) <= MAX_SEL_LEN) { 31 | entry.AppendText(selection) 32 | } 33 | for i := len(search_history) - 1; i >= 0; i-- { 34 | entry.AppendText(search_history[i]) 35 | } 36 | entry.SetActive(0) 37 | return entry 38 | } 39 | 40 | func get_source() string { 41 | var be, en gtk.TextIter 42 | source_buf.GetStartIter(&be) 43 | source_buf.GetEndIter(&en) 44 | return source_buf.GetText(&be, &en, true) 45 | } 46 | 47 | func file_save_current() { 48 | var be, en gtk.TextIter 49 | rec, found := file_map[cur_file] 50 | if false == found { 51 | rec = new(FileRecord) 52 | } 53 | rec.buf = ([]byte)(get_source()) 54 | rec.modified = source_buf.GetModified() 55 | source_buf.GetSelectionBounds(&be, &en) 56 | rec.sel_be = be.GetOffset() 57 | rec.sel_en = en.GetOffset() 58 | file_stack_push(cur_file) 59 | if found { 60 | runtime.GC() 61 | } 62 | } 63 | 64 | // Switches to another file. In most cases you want to call file_save_current 65 | // before this method. Otherwise current changes will be lost. 66 | func file_switch_to(name string) { 67 | if "" == name { 68 | return 69 | } 70 | tree_view_set_cur_iter(false) 71 | rec, found := file_map[name] 72 | var text_to_set string 73 | var modified_to_set bool 74 | var name_to_set string 75 | var sel_be_to_set, sel_en_to_set int 76 | if found { 77 | text_to_set = string(rec.buf) 78 | modified_to_set = rec.modified 79 | name_to_set = name 80 | sel_be_to_set = rec.sel_be 81 | sel_en_to_set = rec.sel_en 82 | } else { 83 | text_to_set = "" 84 | modified_to_set = true 85 | name_to_set = "" 86 | sel_be_to_set = 0 87 | sel_en_to_set = 0 88 | } 89 | cur_file = name_to_set 90 | tree_view_set_cur_iter(true) 91 | source_buf.BeginNotUndoableAction() 92 | source_buf.SetText(text_to_set) 93 | source_buf.SetModified(modified_to_set) 94 | source_buf.EndNotUndoableAction() 95 | refresh_title() 96 | source_view.GrabFocus() 97 | var be_iter, en_iter gtk.TextIter 98 | source_buf.GetIterAtOffset(&be_iter, sel_be_to_set) 99 | source_buf.GetIterAtOffset(&en_iter, sel_en_to_set) 100 | move_focus_and_selection(&be_iter, &en_iter) 101 | 102 | prev_selection = "" 103 | mark_set_cb() 104 | lang_refresh() 105 | } 106 | 107 | func file_stack_push(name string) { 108 | if name == file_stack_at_top() { 109 | return 110 | } 111 | file_stack[file_stack_top] = name 112 | if file_stack_top == file_stack_max { 113 | stack_next(&file_stack_max) 114 | } 115 | stack_next(&file_stack_top) 116 | if file_stack_top == file_stack_base { 117 | stack_next(&file_stack_base) 118 | } 119 | } 120 | 121 | func file_stack_pop() string { 122 | for { 123 | if file_stack_base == file_stack_top { 124 | return "" 125 | } 126 | stack_prev(&file_stack_top) 127 | res := file_stack[file_stack_top] 128 | if file_opened(res) { 129 | return res 130 | } 131 | } 132 | return "" 133 | } 134 | 135 | func stack_next(a *int) { 136 | *a++ 137 | if STACK_SIZE == *a { 138 | *a = 0 139 | } 140 | } 141 | 142 | func stack_prev(a *int) { 143 | *a-- 144 | if -1 == *a { 145 | *a = STACK_SIZE - 1 146 | } 147 | } 148 | 149 | func mark_set_cb() { 150 | var cur gtk.TextIter 151 | var be, en gtk.TextIter 152 | 153 | source_buf.GetSelectionBounds(&be, &en) 154 | selection := source_buf.GetSlice(&be, &en, false) 155 | if prev_selection == selection { 156 | return 157 | } 158 | prev_selection = selection 159 | 160 | if selection_flag { 161 | source_buf.GetStartIter(&be) 162 | source_buf.GetEndIter(&en) 163 | source_buf.RemoveTagByName("instance", &be, &en) 164 | selection_flag = false 165 | } 166 | 167 | sel_len := len(selection) 168 | if (sel_len <= 1) || (sel_len >= MAX_SEL_LEN) { 169 | return 170 | } else { 171 | selection_flag = true 172 | } 173 | 174 | source_buf.GetStartIter(&cur) 175 | for cur.ForwardSearch(selection, 0, &be, &cur, nil) { 176 | source_buf.ApplyTagByName("instance", &be, &cur) 177 | } 178 | } 179 | 180 | func find_next_instance(start, be, en *gtk.TextIter, pattern string) bool { 181 | if start.ForwardSearch(pattern, 0, be, en, nil) { 182 | return true 183 | } 184 | source_buf.GetStartIter(be) 185 | return be.ForwardSearch(pattern, 0, be, en, nil) 186 | } 187 | 188 | func next_instance_cb() { 189 | var be, en gtk.TextIter 190 | source_buf.GetSelectionBounds(&be, &en) 191 | selection := source_buf.GetSlice(&be, &en, false) 192 | if "" == selection { 193 | return 194 | } 195 | // find_next_instance cannot return false because selection is not empty. 196 | find_next_instance(&en, &be, &en, selection) 197 | move_focus_and_selection(&be, &en) 198 | } 199 | 200 | func find_prev_instance(start, be, en *gtk.TextIter, pattern string) bool { 201 | if start.BackwardSearch(pattern, 0, be, en, nil) { 202 | return true 203 | } 204 | source_buf.GetEndIter(be) 205 | return be.BackwardSearch(pattern, 0, be, en, nil) 206 | } 207 | 208 | func prev_instance_cb() { 209 | var be, en gtk.TextIter 210 | source_buf.GetSelectionBounds(&be, &en) 211 | selection := source_buf.GetSlice(&be, &en, false) 212 | if "" == selection { 213 | return 214 | } 215 | // find_prev_instance cannot return false because selection is not empty. 216 | find_prev_instance(&be, &be, &en, selection) 217 | move_focus_and_selection(&be, &en) 218 | } 219 | 220 | func move_focus_and_selection(be *gtk.TextIter, en *gtk.TextIter) { 221 | source_buf.MoveMarkByName("insert", be) 222 | source_buf.MoveMarkByName("selection_bound", en) 223 | mark := source_buf.GetMark("insert") 224 | source_view.ScrollToMark(mark, 0, true, 1, 0.5) 225 | } 226 | 227 | func tree_view_scroll_to_cur_iter() { 228 | if "" == cur_file { 229 | return 230 | } 231 | //if false == tree_store.IterIsValid(&cur_iter) { 232 | // return 233 | //} 234 | path := tree_model.GetPath(&cur_iter) 235 | tree_view.ScrollToCell(path, nil, true, 0.5, 0) 236 | } 237 | 238 | func source_selection() string { 239 | var be, en gtk.TextIter 240 | source_buf.GetSelectionBounds(&be, &en) 241 | return source_buf.GetSlice(&be, &en, false) 242 | } 243 | 244 | func next_file_cb() { 245 | if file_stack_top == file_stack_max { 246 | return 247 | } 248 | cur := file_stack_top 249 | for stack_next(&cur); ; stack_next(&cur) { 250 | if file_opened(file_stack[cur]) { 251 | file_save_current() 252 | file_switch_to(file_stack[cur]) 253 | file_stack_top = cur 254 | return 255 | } 256 | if cur == file_stack_max { 257 | break 258 | } 259 | } 260 | } 261 | 262 | func prev_file_cb() { 263 | shift_flag := file_stack_top == file_stack_max 264 | file_save_current() 265 | if shift_flag { 266 | stack_prev(&file_stack_max) 267 | } 268 | // Popping out cur_file pushed in file_save_current. 269 | // Wrong in case of "" is cur_file !!! 270 | file_stack_pop() 271 | file_switch_to(file_stack_pop()) 272 | } 273 | 274 | func file_stack_at_top() string { 275 | t := file_stack_top 276 | stack_prev(&t) 277 | return file_stack[t] 278 | } 279 | 280 | func init_navigation() { 281 | accel_group = gtk.NewAccelGroup() 282 | } 283 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "strconv" 7 | ) 8 | 9 | type Options struct { 10 | show_error, show_search, space_not_tab bool 11 | ohp_position, ihp_position, vvp_position int 12 | window_width, window_height, window_x, window_y int 13 | font string 14 | tabsize int 15 | } 16 | 17 | func new_options() (o Options) { 18 | o.show_search = true 19 | o.show_error = true 20 | o.ihp_position = 150 21 | o.ohp_position = 670 22 | o.vvp_position = 375 23 | o.window_width, o.window_height = 800, 510 24 | o.window_x, o.window_y = 0, 0 25 | o.font = "Monospace Regular 10" 26 | o.tabsize = 2 27 | return 28 | } 29 | 30 | func atoi(s string) (i int) { 31 | i, _ = strconv.Atoi(s) 32 | return 33 | } 34 | 35 | func load_options() { 36 | reader, file := take_reader_from_file(os.Getenv("HOME") + "/.tabbyoptions") 37 | defer file.Close() 38 | var str string 39 | for next_string_from_reader(reader, &str) { 40 | args := strings.Split(compact_space(str), "\t") 41 | switch args[0] { 42 | case "space_not_tab": 43 | opt.space_not_tab, _ = strconv.ParseBool(args[1]) 44 | case "show_search": 45 | opt.show_search, _ = strconv.ParseBool(args[1]) 46 | case "show_error": 47 | opt.show_error, _ = strconv.ParseBool(args[1]) 48 | case "ihp_position": 49 | opt.ihp_position = atoi(args[1]) 50 | case "ohp_position": 51 | opt.ohp_position = atoi(args[1]) 52 | case "vvp_position": 53 | opt.vvp_position = atoi(args[1]) 54 | case "alloc_window": 55 | opt.window_width, opt.window_height, opt.window_x, opt.window_y = atoi(args[1]), 56 | atoi(args[2]), atoi(args[3]), atoi(args[4]) 57 | case "font": 58 | opt.font = args[1] 59 | case "tabsize": 60 | opt.tabsize = atoi(args[1]) 61 | } 62 | } 63 | } 64 | 65 | func save_options() { 66 | file, _ := os.OpenFile(os.Getenv("HOME")+"/.tabbyoptions", os.O_CREATE|os.O_WRONLY, 0644) 67 | if nil == file { 68 | tabby_log("unable to save options") 69 | return 70 | } 71 | file.Truncate(0) 72 | file.WriteString("show_search\t" + strconv.FormatBool(opt.show_search) + "\n") 73 | file.WriteString("show_error\t" + strconv.FormatBool(opt.show_error) + "\n") 74 | file.WriteString("space_not_tab\t" + strconv.FormatBool(opt.space_not_tab) + "\n") 75 | file.WriteString("ihp_position\t" + strconv.Itoa(opt.ihp_position) + "\n") 76 | file.WriteString("ohp_position\t" + strconv.Itoa(opt.ohp_position) + "\n") 77 | file.WriteString("vvp_position\t" + strconv.Itoa(opt.vvp_position) + "\n") 78 | file.WriteString("alloc_window\t" + strconv.Itoa(opt.window_width) + "\t" + 79 | strconv.Itoa(opt.window_height) + "\t" + strconv.Itoa(opt.window_x) + "\t" + 80 | strconv.Itoa(opt.window_y) + "\n") 81 | file.WriteString("font\t" + opt.font + "\n") 82 | file.WriteString("tabsize\t" + strconv.Itoa(opt.tabsize) + "\n") 83 | file.Close() 84 | } 85 | 86 | func compact_space(s string) string { 87 | s = strings.TrimSpace(s) 88 | n := replace_space(s) 89 | for n != s { 90 | s = n 91 | n = replace_space(s) 92 | } 93 | return s 94 | } 95 | 96 | func replace_space(s string) string { 97 | return strings.Replace(strings.Replace(strings.Replace(s, " ", " ", -1), 98 | "\t ", "\t", -1), 99 | " \t", "\t", -1) 100 | } 101 | 102 | var opt Options = new_options() 103 | 104 | func window_event_cb() { 105 | main_window.GetSize(&opt.window_width, &opt.window_height) 106 | main_window.GetPosition(&opt.window_x, &opt.window_y) 107 | // TODO: Decide where to place these initialization. 108 | source_view.ModifyFontEasy(opt.font) 109 | options_set_tabsize(opt.tabsize) 110 | } 111 | 112 | func ohp_cb(pos int) { 113 | opt.ohp_position = pos 114 | } 115 | 116 | func ihp_cb(pos int) { 117 | opt.ihp_position = pos 118 | } 119 | 120 | func vvp_cb(pos int) { 121 | opt.vvp_position = pos 122 | } 123 | 124 | func options_set_tabsize(s int) { 125 | opt.tabsize = s 126 | source_view.SetIndentWidth(s) 127 | source_view.SetTabWidth(uint(s)) 128 | } 129 | -------------------------------------------------------------------------------- /resource/.tabbyignore: -------------------------------------------------------------------------------- 1 | [A-Za-z0-9/_\-]*[.]svn 2 | [A-Za-z0-9/_\-]*[.]git 3 | .*~ 4 | .*\.bak 5 | -------------------------------------------------------------------------------- /resource/classic.xml: -------------------------------------------------------------------------------- 1 | 2 | 23 | 24 | GtkSourceView team 25 | <_description>Classic color scheme 26 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |