├── .gitignore ├── LICENSE ├── README.md ├── cnote.go ├── main.go ├── sort.go └── util.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | *.directory 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Wei Shen (shenwei356@gmail.com) 2 | 3 | The MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | cnote 2 | ===== 3 | 4 | A **platform independent** command line note app. 5 | 6 | Cnote is suitable for **rapidly taking and getting note** of plain text of tens of words. 7 | 8 | With cnote, you can conveniently manage note in command line. You can install a drop-down terminal like [Yakuake](http://yakuake.kde.org), so you can instantly call the terminal with a shortcut. 9 | 10 | Cnote supports **backup** and **restoring** from backup, you can also **import** notes from backup of others. 11 | 12 | Cnote stores all data in a embedded database [goleveldb](https://github.com/syndtr/goleveldb), an implementation of the LevelDB key/value database in the Go programming language. The path of database files is ```~/.cnote/``` in *nix operating system and ```C:\Users\Administrator\.cnote\``` in Windows 7 for example. 13 | 14 | Dependencies 15 | ------------ 16 | 17 | No. Thanks for [golang](http://golang.org) and [goleveldb](https://github.com/syndtr/goleveldb). 18 | 19 | Cnote is **only an executable binary file** without any data or configure files. 20 | 21 | Download 22 | -------- 23 | 24 | Download lastest release from [release page](https://github.com/shenwei356/cnote/releases). 25 | 26 | Usage 27 | ----- 28 | 29 | USAGE: 30 | cnote command [arguments...] 31 | 32 | COMMANDS: 33 | new Create a new note 34 | del Delete a note 35 | use Select a note 36 | list, ls List all notes 37 | 38 | add Add a note item 39 | rm Remove a note item 40 | tag, t List items by tags. List all tags if no arguments given 41 | search, s Search items with regular expression 42 | 43 | dump Dump whole database, for backup or transfer 44 | wipe Attention! Wipe whole database 45 | restore Wipe whole database, and restore from dumpped file 46 | import Import note items from dumpped data 47 | 48 | help, h Shows a list of commands or help for one command 49 | 50 | 51 | Examples 52 | -------- 53 | 54 | ############### Create a new note ############### 55 | 56 | $ cnote new fruit 57 | note "fruit" created. 58 | current note: "fruit". 59 | $ cnote new people 60 | note "people" created. 61 | current note: "people". 62 | 63 | ############### List all notes ############### 64 | 65 | $ cnote ls 66 | note: fruit (#. of items: 0, last update: 2014-07-20 04:07:00 +0800 CST). 67 | note: people (#. of items: 0, last update: 2014-07-20 04:07:00 +0800 CST). (current note) 68 | 69 | ############### Choose note fruit ############### 70 | 71 | $ cnote use fruit 72 | current note: "fruit" (last update: 2014-07-20 04:07:00 +0800 CST). 73 | 74 | ############### Delete a new note ############### 75 | 76 | $ cnote del test 77 | 78 | ########################################################################### 79 | 80 | ############### add note item ############### 81 | 82 | $ cnote add red,green apple 83 | item: 1 (tags: [red green]) apple 84 | $ cnote add green,yellow pear 85 | item: 2 (tags: [green yellow]) pear 86 | $ cnote add yellow banana 87 | item: 3 (tags: [yellow]) banana 88 | 89 | ############### Show all tags ############### 90 | 91 | $ cnote tag 92 | tag: green (#. of items: 2). 93 | tag: red (#. of items: 1). 94 | tag: yellow (#. of items: 2). 95 | 96 | ############### Show items by tag ############### 97 | 98 | $ cnote tag yellow 99 | item: 2 (tags: [green yellow]) pear 100 | item: 3 (tags: [yellow]) banana 101 | 102 | ############### Search items by regrexp ############### 103 | 104 | $ cnote s ea 105 | item: 2 (tags: [green yellow]) pear 106 | 107 | ############### Show all items, just search with . ############### 108 | 109 | $ cnote s . 110 | item: 1 (tags: [red green]) apple 111 | item: 2 (tags: [green yellow]) pear 112 | item: 3 (tags: [yellow]) banana 113 | 114 | ############### remove a note item ############### 115 | 116 | $ cnote s . 117 | item: 1 (tags: [red green]) apple 118 | item: 2 (tags: [green yellow]) pear 119 | item: 3 (tags: [yellow]) banana 120 | $ cnote rm 2 121 | $ cnote s . 122 | item: 1 (tags: [red green]) apple 123 | item: 3 (tags: [yellow]) banana 124 | 125 | ########################################################################### 126 | 127 | ############### Dump database for backup ############### 128 | 129 | $ cnote dump 130 | config {"current_note_name":"fruit"} 131 | item_fruit_000000001 {"itemid":"1","tags":["red","green"],"content":"apple"} 132 | item_fruit_000000002 {"itemid":"2","tags":["green","yellow"],"content":"pear"} 133 | item_fruit_000000003 {"itemid":"3","tags":["yellow"],"content":"banana"} 134 | note_fruit {"noteid":"fruit","sum":3,"last_update":"2014-07-20 04:13:00 +0800 CST","last_id":3,"tags":{"green":{"1":true,"2":true},"red":{"1":true},"yellow":{"2":true,"3":true}}} 135 | note_people {"noteid":"people","sum":0,"last_update":"2014-07-20 04:07:00 +0800 CST","last_id":0,"tags":{}} 136 | 137 | $ cnote dump > dumpdata 138 | 139 | ############### Wipe whole database, and restore from dumpped file ############### 140 | 141 | $ cnote restore dumpdata 142 | Attention, it will clear all the data. type "yes" to continue:yes 143 | 144 | ############### Import note items from dumpped data ############### 145 | 146 | $ cnote import fruit fruit dumpdata 147 | 3 items imported into note "fruit". 148 | $ cnote dump 149 | config {"current_note_name":"fruit"} 150 | item_fruit_000000001 {"itemid":"1","tags":["red","green"],"content":"apple"} 151 | item_fruit_000000002 {"itemid":"2","tags":["green","yellow"],"content":"pear"} 152 | item_fruit_000000003 {"itemid":"3","tags":["yellow"],"content":"banana"} 153 | item_fruit_000000004 {"itemid":"4","tags":["red","green"],"content":"apple"} 154 | item_fruit_000000005 {"itemid":"5","tags":["green","yellow"],"content":"pear"} 155 | item_fruit_000000006 {"itemid":"6","tags":["yellow"],"content":"banana"} 156 | note_fruit {"noteid":"fruit","sum":6,"last_update":"2014-07-20 04:22:00 +0800 CST","last_id":6,"tags":{"green":{"1":true,"2":true,"4":true,"5":true},"red":{"1":true,"4":true},"yellow":{"2":true,"3":true,"5":true,"6":true}}} 157 | note_people {"noteid":"people","sum":0,"last_update":"2014-07-20 04:07:00 +0800 CST","last_id":0,"tags":{}} 158 | 159 | 160 | Copyright 161 | -------- 162 | 163 | Copyright (c) 2014, Wei Shen (shenwei356@gmail.com) 164 | 165 | 166 | [MIT License](https://github.com/shenwei356/cnote/blob/master/LICENSE) 167 | -------------------------------------------------------------------------------- /cnote.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "os" 10 | "regexp" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/jinzhu/now" 16 | "github.com/syndtr/goleveldb/leveldb" 17 | "github.com/syndtr/goleveldb/leveldb/util" 18 | ) 19 | 20 | var ( 21 | NOTE_PREFIX = "note_" 22 | ITEM_PREFIX = "item_" 23 | ) 24 | 25 | type Config struct { 26 | CurrentNoteName string `json:"current_note_name"` 27 | } 28 | 29 | func (conf *Config) Clone() *Config { 30 | c := new(Config) 31 | c.CurrentNoteName = conf.CurrentNoteName 32 | return c 33 | } 34 | 35 | func (conf *Config) IsEqualTo(c *Config) bool { 36 | if conf.CurrentNoteName != c.CurrentNoteName { 37 | return false 38 | } 39 | return true 40 | } 41 | 42 | type Note struct { 43 | NoteID string `json:"noteid"` 44 | Sum int `json:"sum"` 45 | LastUpdate string `json:"last_update"` 46 | LastId int `json:"last_id"` 47 | // not that, the type of the key of interal map is string 48 | // because the leveldb only allow string as key. 49 | Tags map[string]map[string]bool `json:"tags"` 50 | 51 | Items map[int]*Item `json:"-"` 52 | } 53 | 54 | type Item struct { 55 | ItemID string `json:"itemid"` 56 | Tags []string `json:"tags"` 57 | Content string `json:"content"` 58 | } 59 | 60 | func (item *Item) String() string { 61 | return fmt.Sprintf("item: %s\t(tags: %v)\t%s", 62 | item.ItemID, item.Tags, item.Content) 63 | } 64 | 65 | type NoteDB struct { 66 | Config *Config 67 | NotesList []string 68 | CurrentNote *Note 69 | 70 | db *leveldb.DB 71 | dbfile string 72 | 73 | oldConfig *Config 74 | } 75 | 76 | func NewNoteDB(dbfile string) *NoteDB { 77 | notedb := new(NoteDB) 78 | notedb.dbfile = dbfile 79 | 80 | notedb.ConnectDB() 81 | 82 | notedb.ReadConfig() 83 | notedb.oldConfig = notedb.Config.Clone() 84 | 85 | // check the config 86 | _, err := notedb.ReadNote(notedb.Config.CurrentNoteName) 87 | if err != nil { 88 | notedb.Config.CurrentNoteName = "" 89 | } 90 | 91 | err = notedb.UseNote(notedb.Config.CurrentNoteName) 92 | if err != nil { 93 | fmt.Println(err) 94 | os.Exit(0) 95 | } 96 | 97 | list, err := notedb.GetNotesList() 98 | if err != nil { 99 | fmt.Println(err) 100 | os.Exit(0) 101 | } 102 | notedb.NotesList = list 103 | 104 | /* if len(list) == 0 { 105 | fmt.Println("no notes in database, please create one.") 106 | }*/ 107 | 108 | return notedb 109 | } 110 | 111 | ////////////////////////////////////////////////////// 112 | 113 | func (notedb *NoteDB) ConnectDB() { 114 | db, err := leveldb.OpenFile(notedb.dbfile, nil) 115 | if err != nil { 116 | fmt.Sprintf("fail to open leveldb file: %s. %s", notedb.dbfile, err) 117 | os.Exit(0) 118 | } 119 | notedb.db = db 120 | } 121 | 122 | func (notedb *NoteDB) Close() { 123 | if !notedb.Config.IsEqualTo(notedb.oldConfig) { 124 | notedb.SaveConfig() 125 | } 126 | notedb.db.Close() 127 | } 128 | 129 | ////////////////////////////////////////////////////// 130 | 131 | func (notedb *NoteDB) GetCurrentNote() (*Note, error) { 132 | if notedb.CurrentNote == nil { 133 | return nil, errors.New( 134 | fmt.Sprintf("no note choosed from %v. Use \"cnote use notename\".", 135 | notedb.NotesList)) 136 | } 137 | return notedb.CurrentNote, nil 138 | } 139 | 140 | func (notedb *NoteDB) ReadNote(notename string) (*Note, error) { 141 | var note = &Note{} 142 | key := NOTE_PREFIX + notename 143 | 144 | err := notedb.ReadStruct(key, note) 145 | if err != nil { 146 | return nil, errors.New( 147 | fmt.Sprintf("note \"%s\" not exist.", notename)) 148 | } 149 | 150 | return note, nil 151 | } 152 | 153 | func (notedb *NoteDB) SaveNote(note *Note) error { 154 | key := NOTE_PREFIX + note.NoteID 155 | err := notedb.SaveStruct(key, note) 156 | if err != nil { 157 | return errors.New(fmt.Sprintf("fail to save %s. %v", key, err)) 158 | } 159 | 160 | return nil 161 | } 162 | 163 | func (notedb *NoteDB) NewNote(notename string) error { 164 | 165 | // check whether note exists 166 | _, err := notedb.ReadNote(notename) 167 | if err == nil { 168 | return errors.New( 169 | fmt.Sprintf("note \"%s\" already exist.", notename)) 170 | } 171 | 172 | note := &Note{ 173 | NoteID: notename, 174 | Sum: 0, 175 | LastUpdate: now.BeginningOfMinute().String(), 176 | LastId: 0, 177 | Tags: map[string]map[string]bool{}, 178 | } 179 | 180 | notedb.NotesList = append(notedb.NotesList, notename) 181 | 182 | // save note 183 | err = notedb.SaveNote(note) 184 | if err != nil { 185 | return err 186 | } 187 | 188 | err = notedb.UseNote(notename) 189 | if err != nil { 190 | return err 191 | } 192 | return nil 193 | } 194 | 195 | func (notedb *NoteDB) DeleteNote(notename string) error { 196 | 197 | // read note 198 | note, err := notedb.ReadNote(notename) 199 | if err != nil { 200 | return err 201 | } 202 | 203 | // first, remove all items of the note 204 | itemids := make(map[int]bool, 0) 205 | for tag, _ := range note.Tags { 206 | for itemid, _ := range note.Tags[tag] { 207 | 208 | itemid, err := strconv.Atoi(itemid) 209 | if err != nil { 210 | return err 211 | } 212 | 213 | itemids[itemid] = true 214 | } 215 | } 216 | for itemid, _ := range itemids { 217 | err := notedb.RemoveNoteItem(note, itemid) 218 | if err != nil { 219 | return err 220 | } 221 | } 222 | 223 | // second, delete the note 224 | key := NOTE_PREFIX + note.NoteID 225 | err = notedb.DeleteStruct(key) 226 | if err != nil { 227 | return err 228 | } 229 | 230 | // update list 231 | list := make([]string, 0) 232 | for _, n := range notedb.NotesList { 233 | if n != notename { 234 | list = append(list, n) 235 | } 236 | } 237 | notedb.NotesList = list 238 | 239 | // update config 240 | notedb.Config.CurrentNoteName = "" 241 | notedb.CurrentNote = nil 242 | 243 | return nil 244 | } 245 | 246 | func (notedb *NoteDB) UseNote(notename string) error { 247 | 248 | // not note exists 249 | if notename == "" && len(notedb.NotesList) == 0 { 250 | notedb.Config.CurrentNoteName = notename 251 | notedb.CurrentNote = nil 252 | return nil 253 | } 254 | 255 | // read note 256 | note, err := notedb.ReadNote(notename) 257 | if err != nil { 258 | return err 259 | } 260 | 261 | notedb.Config.CurrentNoteName = notename 262 | notedb.CurrentNote = note 263 | 264 | return nil 265 | } 266 | 267 | func (notedb *NoteDB) GetNotesList() ([]string, error) { 268 | list := make([]string, 0) 269 | iter := notedb.db.NewIterator(&util.Range{Start: []byte(NOTE_PREFIX)}, nil) 270 | for iter.Next() { 271 | key := iter.Key() 272 | list = append(list, trim_prefix(NOTE_PREFIX, string(key))) 273 | } 274 | iter.Release() 275 | err := iter.Error() 276 | return list, err 277 | } 278 | 279 | func (notedb *NoteDB) AddNoteItem(tagstring, content string) (*Item, error) { 280 | note, err := notedb.GetCurrentNote() 281 | if err != nil { 282 | return nil, err 283 | } 284 | 285 | tags := make([]string, 0) 286 | re := regexp.MustCompile(`^\s*$`) 287 | for _, tag := range strings.Split(tagstring, ",") { 288 | // empty 289 | if re.MatchString(tag) { 290 | continue 291 | } 292 | 293 | tags = append(tags, tag) 294 | } 295 | 296 | notedb.CurrentNote.LastId++ 297 | 298 | item := &Item{ 299 | ItemID: fmt.Sprintf("%d", notedb.CurrentNote.LastId), 300 | Tags: tags, 301 | Content: content, 302 | } 303 | 304 | // save item 305 | key := fmt.Sprintf("%s%s_%09d", ITEM_PREFIX, 306 | notedb.CurrentNote.NoteID, notedb.CurrentNote.LastId) 307 | err = notedb.SaveStruct(key, item) 308 | if err != nil { 309 | return nil, errors.New(fmt.Sprintf("fail to save %s. %v", key, err)) 310 | } 311 | 312 | // update current note 313 | note.Sum++ 314 | for _, tag := range tags { 315 | if _, ok := note.Tags[tag]; !ok { 316 | note.Tags[tag] = make(map[string]bool, 0) 317 | } 318 | 319 | note.Tags[tag][item.ItemID] = true 320 | } 321 | note.LastUpdate = now.BeginningOfMinute().String() 322 | 323 | // save note 324 | err = notedb.SaveNote(note) 325 | if err != nil { 326 | return nil, err 327 | } 328 | 329 | return item, nil 330 | } 331 | 332 | func (notedb *NoteDB) ReadNoteItem(note *Note, itemid int) (*Item, error) { 333 | if note == nil { 334 | return nil, errors.New( 335 | fmt.Sprintf("no note choosed from %v. Use \"cnote use notename\".", 336 | notedb.NotesList)) 337 | } 338 | 339 | var item = &Item{} 340 | key := fmt.Sprintf("%s%s_%09d", ITEM_PREFIX, 341 | note.NoteID, itemid) 342 | err := notedb.ReadStruct(key, item) 343 | if err != nil { 344 | return nil, errors.New( 345 | fmt.Sprintf("item \"%d\" not exist in note \"%s\".", 346 | itemid, note.NoteID)) 347 | } 348 | 349 | return item, nil 350 | } 351 | 352 | func (notedb *NoteDB) RemoveNoteItem(note *Note, itemid int) error { 353 | item, err := notedb.ReadNoteItem(note, itemid) 354 | if err != nil { 355 | return err 356 | } 357 | 358 | key := fmt.Sprintf("%s%s_%09d", ITEM_PREFIX, 359 | note.NoteID, itemid) 360 | 361 | err = notedb.DeleteStruct(key) 362 | if err != nil { 363 | return err 364 | } 365 | 366 | // update note 367 | note.Sum-- 368 | for _, tag := range item.Tags { 369 | delete(note.Tags[tag], item.ItemID) 370 | 371 | if len(note.Tags[tag]) == 0 { 372 | delete(note.Tags, tag) 373 | } 374 | } 375 | note.LastUpdate = now.BeginningOfMinute().String() 376 | 377 | // save note 378 | err = notedb.SaveNote(note) 379 | if err != nil { 380 | return err 381 | } 382 | 383 | return nil 384 | } 385 | 386 | func (notedb *NoteDB) ItemByRegexp(queries []string) ([]*Item, error) { 387 | note, err := notedb.GetCurrentNote() 388 | if err != nil { 389 | return nil, err 390 | } 391 | 392 | // read all items 393 | if note.Items == nil { 394 | note.Items = make(map[int]*Item, 0) 395 | } 396 | for tag, _ := range note.Tags { 397 | for itemid, _ := range note.Tags[tag] { 398 | itemid, err := strconv.Atoi(itemid) 399 | if err != nil { 400 | return nil, err 401 | } 402 | 403 | if _, ok := note.Items[itemid]; ok { // already loaded 404 | continue 405 | } 406 | 407 | item, err := notedb.ReadNoteItem(note, itemid) 408 | if err != nil { 409 | return nil, err 410 | } 411 | 412 | note.Items[itemid] = item 413 | } 414 | } 415 | 416 | // query by regexp 417 | items := make([]*Item, 0) 418 | for _, query := range queries { 419 | re := regexp.MustCompile(query) 420 | 421 | // sort itemids 422 | itemids := make([]int, 0) 423 | for itemid, _ := range note.Items { 424 | itemids = append(itemids, itemid) 425 | } 426 | sort.Ints(itemids) 427 | 428 | for _, itemid := range itemids { 429 | item := note.Items[itemid] 430 | if re.MatchString(item.Content) { 431 | items = append(items, item) 432 | } 433 | } 434 | } 435 | 436 | return items, nil 437 | } 438 | 439 | func (notedb *NoteDB) ItemByTag(tags []string) ([]*Item, error) { 440 | note, err := notedb.GetCurrentNote() 441 | if err != nil { 442 | return nil, err 443 | } 444 | 445 | items := make([]*Item, 0) 446 | 447 | for _, tag := range tags { 448 | if _, ok := note.Tags[tag]; !ok { 449 | fmt.Printf("tag \"%s\" not exist in note \"%s\".\n", tag, note.NoteID) 450 | continue 451 | } 452 | 453 | // sort itemids 454 | itemids := make([]int, 0) 455 | for itemid, _ := range note.Tags[tag] { 456 | itemid, err := strconv.Atoi(itemid) 457 | if err != nil { 458 | return nil, err 459 | } 460 | itemids = append(itemids, itemid) 461 | } 462 | sort.Ints(itemids) 463 | 464 | for _, itemid := range itemids { 465 | item, err := notedb.ReadNoteItem(note, itemid) 466 | if err != nil { 467 | return nil, err 468 | } 469 | items = append(items, item) 470 | } 471 | } 472 | 473 | return items, nil 474 | } 475 | 476 | ////////////////////////////////////////////////////////////////////// 477 | 478 | func (notedb *NoteDB) ReadConfig() { 479 | notedb.Config = &Config{} 480 | 481 | err := notedb.ReadStruct("config", notedb.Config) 482 | if err != nil { // no config 483 | notedb.Config = &Config{CurrentNoteName: ""} 484 | return 485 | } 486 | } 487 | 488 | func (notedb *NoteDB) SaveConfig() { 489 | err := notedb.SaveStruct("config", notedb.Config) 490 | if err != nil { 491 | fmt.Sprintf("fail to save config. %v", err) 492 | os.Exit(0) 493 | } 494 | } 495 | 496 | ////////////////////////////////////////////////////////////////////// 497 | 498 | func (notedb *NoteDB) ReadStruct(key string, str interface{}) error { 499 | data, err := notedb.db.Get([]byte(key), nil) 500 | if err != nil { // not exist 501 | return err 502 | } 503 | if err := json.Unmarshal(data, str); err != nil { 504 | return err 505 | } 506 | 507 | return nil 508 | } 509 | 510 | func (notedb *NoteDB) SaveStruct(key string, str interface{}) error { 511 | bytes, err := json.Marshal(str) 512 | if err != nil { 513 | return err 514 | } 515 | 516 | err = notedb.db.Put([]byte(key), bytes, nil) 517 | if err != nil { 518 | return err 519 | } 520 | 521 | // fmt.Printf("save %s: %s\n", key, string(bytes)) 522 | return nil 523 | } 524 | 525 | func (notedb *NoteDB) DeleteStruct(key string) error { 526 | err := notedb.db.Delete([]byte(key), nil) 527 | if err != nil { 528 | return err 529 | } 530 | return nil 531 | } 532 | 533 | func (notedb *NoteDB) Dump() error { 534 | iter := notedb.db.NewIterator(nil, nil) 535 | for iter.Next() { 536 | key := iter.Key() 537 | value := iter.Value() 538 | fmt.Printf("%s\t%s\r\n", key, value) 539 | } 540 | iter.Release() 541 | err := iter.Error() 542 | return err 543 | } 544 | 545 | func (notedb *NoteDB) Wipe() error { 546 | for _, notename := range notedb.NotesList { 547 | err := notedb.DeleteNote(notename) 548 | if err != nil { 549 | return err 550 | } 551 | } 552 | return nil 553 | } 554 | 555 | func (notedb *NoteDB) Restore(filename string) error { 556 | // wipe all the database 557 | err := notedb.Wipe() 558 | if err != nil { 559 | return err 560 | } 561 | 562 | fh, err := os.Open(filename) 563 | if err != nil { 564 | return errors.New("fail to open file: " + filename) 565 | } 566 | 567 | batch := new(leveldb.Batch) 568 | reader := bufio.NewReader(fh) 569 | re1 := regexp.MustCompile(`[\r\n]`) 570 | re2 := regexp.MustCompile(`^\s+|\s+$`) 571 | re := regexp.MustCompile(`([^\t]+)\t([^\t]+)`) 572 | for { 573 | str, err := reader.ReadString('\n') 574 | 575 | str = re1.ReplaceAllString(str, "") 576 | str = re2.ReplaceAllString(str, "") 577 | if err == io.EOF { 578 | 579 | if re.MatchString(str) { 580 | data := re.FindSubmatch([]byte(str)) 581 | batch.Put(data[1], data[2]) 582 | } 583 | 584 | break 585 | } 586 | 587 | if re.MatchString(str) { 588 | data := re.FindSubmatch([]byte(str)) 589 | batch.Put(data[1], data[2]) 590 | } 591 | } 592 | 593 | err = notedb.db.Write(batch, nil) 594 | if err != nil { 595 | return nil 596 | } 597 | 598 | return nil 599 | } 600 | 601 | func (notedb *NoteDB) Import(notename, othernotename, filename string) (int, error) { 602 | err := notedb.UseNote(notename) 603 | if err != nil { 604 | return 0, err 605 | } 606 | 607 | fh, err := os.Open(filename) 608 | if err != nil { 609 | return 0, errors.New("fail to open file: " + filename) 610 | } 611 | 612 | batch := new(leveldb.Batch) 613 | reader := bufio.NewReader(fh) 614 | re1 := regexp.MustCompile(`[\r\n]`) 615 | re2 := regexp.MustCompile(`^\s+|\s+$`) 616 | re := regexp.MustCompile(`([^\t]+)\t([^\t]+)`) 617 | 618 | n := 0 619 | 620 | for { 621 | str, err := reader.ReadString('\n') 622 | 623 | str = re1.ReplaceAllString(str, "") 624 | str = re2.ReplaceAllString(str, "") 625 | if err == io.EOF { 626 | 627 | if !re.MatchString(str) { 628 | break 629 | } 630 | 631 | data := re.FindSubmatch([]byte(str)) 632 | key := string(data[1]) 633 | value := data[2] 634 | 635 | if !strings.HasPrefix(key, ITEM_PREFIX) { 636 | break 637 | } 638 | 639 | key = trim_prefix(ITEM_PREFIX, key) 640 | if !strings.HasPrefix(key, othernotename) { 641 | break 642 | } 643 | 644 | item := &Item{} 645 | if err := json.Unmarshal(value, item); err != nil { 646 | return 0, err 647 | } 648 | 649 | _, err = notedb.AddNoteItem( 650 | strings.Join(item.Tags, ","), item.Content) 651 | if err != nil { 652 | return 0, err 653 | } 654 | 655 | n++ 656 | break 657 | } 658 | 659 | if !re.MatchString(str) { 660 | break 661 | } 662 | 663 | data := re.FindSubmatch([]byte(str)) 664 | key := string(data[1]) 665 | value := data[2] 666 | if !strings.HasPrefix(key, ITEM_PREFIX) { 667 | continue 668 | } 669 | 670 | key = trim_prefix(ITEM_PREFIX, key) 671 | if !strings.HasPrefix(key, othernotename) { 672 | continue 673 | } 674 | 675 | item := &Item{} 676 | if err := json.Unmarshal(value, item); err != nil { 677 | return 0, err 678 | } 679 | 680 | _, err = notedb.AddNoteItem( 681 | strings.Join(item.Tags, ","), item.Content) 682 | if err != nil { 683 | return 0, err 684 | } 685 | 686 | n++ 687 | } 688 | 689 | err = notedb.db.Write(batch, nil) 690 | if err != nil { 691 | return 0, err 692 | } 693 | 694 | return n, nil 695 | } 696 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/user" 7 | "path/filepath" 8 | "sort" 9 | "strconv" 10 | 11 | "github.com/codegangsta/cli" 12 | ) 13 | 14 | var ( 15 | funcs map[string]func(c *cli.Context) 16 | DBFILE string 17 | notedb *NoteDB 18 | ) 19 | 20 | func init() { 21 | // DBFILE 22 | usr, err := user.Current() 23 | if err != nil { 24 | fmt.Println(err) 25 | os.Exit(1) 26 | } 27 | DBFILE = filepath.Join(usr.HomeDir, ".cnote") 28 | 29 | funcs = make(map[string]func(c *cli.Context)) 30 | funcs["new"] = funNew 31 | funcs["del"] = funDel 32 | funcs["use"] = funUse 33 | funcs["list"] = funLs 34 | 35 | funcs["add"] = funAdd 36 | funcs["rm"] = funRm 37 | 38 | funcs["tag"] = funTag 39 | funcs["search"] = funSearch 40 | 41 | funcs["dump"] = funDump 42 | funcs["wipe"] = funWipe 43 | funcs["restore"] = funRestore 44 | funcs["import"] = funImport 45 | 46 | } 47 | 48 | func getFunc(funcs map[string]func(c *cli.Context), name string) func(c *cli.Context) { 49 | if f, ok := funcs[name]; ok { 50 | return f 51 | } else { 52 | return func(c *cli.Context) { 53 | fmt.Printf("command %s not implemented\n", name) 54 | } 55 | } 56 | } 57 | 58 | func funLs(c *cli.Context) { 59 | if len(c.Args()) > 0 { 60 | fmt.Println("no arguments should be given.") 61 | return 62 | } 63 | 64 | for _, notename := range notedb.NotesList { 65 | 66 | // read note 67 | note, err := notedb.ReadNote(notename) 68 | if err != nil { 69 | fmt.Println(err) 70 | return 71 | } 72 | 73 | fmt.Printf("note: %s\t(#. of items: %d, last update: %s).", 74 | notename, note.Sum, note.LastUpdate) 75 | if notedb.CurrentNote != nil && 76 | notename == notedb.CurrentNote.NoteID { 77 | 78 | fmt.Printf(" (current note)") 79 | } 80 | fmt.Println() 81 | } 82 | } 83 | 84 | func funNew(c *cli.Context) { 85 | if len(c.Args()) == 0 { 86 | fmt.Println("note name needed.") 87 | return 88 | } 89 | if len(c.Args()) > 1 { 90 | fmt.Println("only one note name allowed.") 91 | return 92 | } 93 | 94 | notename := c.Args().First() 95 | 96 | err := notedb.NewNote(notename) 97 | if err != nil { 98 | fmt.Println(err) 99 | return 100 | } 101 | 102 | fmt.Printf("note \"%s\" created.\n", notename) 103 | fmt.Printf("current note: \"%s\" (last update: %s).\n", 104 | notename, notedb.CurrentNote.LastUpdate) 105 | } 106 | 107 | func funDel(c *cli.Context) { 108 | if len(c.Args()) == 0 { 109 | fmt.Println("note name needed.") 110 | return 111 | } 112 | notename := c.Args().First() 113 | 114 | note, err := notedb.ReadNote(notename) 115 | if err != nil { 116 | fmt.Println(err) 117 | return 118 | } 119 | 120 | reply, err := request_reply( 121 | fmt.Sprintf("==============================================================\n"+ 122 | " Attention, it will delete all the %d items of note \"%s\".\n"+ 123 | "==============================================================\n", 124 | note.Sum, notename)+ 125 | " Type \"%s\" to continue:", 126 | "yes") 127 | if err != nil { 128 | fmt.Println(err) 129 | return 130 | } 131 | 132 | if reply == false { 133 | return 134 | } 135 | 136 | err = notedb.DeleteNote(notename) 137 | if err != nil { 138 | fmt.Println(err) 139 | return 140 | } 141 | } 142 | 143 | func funUse(c *cli.Context) { 144 | if len(c.Args()) == 0 { 145 | fmt.Println("note name needed.") 146 | return 147 | } 148 | if len(c.Args()) > 1 { 149 | fmt.Println("only one note name allowed.") 150 | return 151 | } 152 | 153 | notename := c.Args().First() 154 | err := notedb.UseNote(notename) 155 | if err != nil { 156 | fmt.Println(err) 157 | return 158 | } 159 | 160 | fmt.Printf("current note: \"%s\" (last update: %s).\n", 161 | notename, notedb.CurrentNote.LastUpdate) 162 | } 163 | 164 | func funAdd(c *cli.Context) { 165 | if len(c.Args()) != 2 { 166 | fmt.Println("tag and content needed.") 167 | return 168 | } 169 | 170 | item, err := notedb.AddNoteItem(c.Args()[0], c.Args()[1]) 171 | if err != nil { 172 | fmt.Println(err) 173 | return 174 | } 175 | 176 | fmt.Println(item) 177 | } 178 | 179 | func funRm(c *cli.Context) { 180 | if len(c.Args()) == 0 { 181 | fmt.Println("item ID needed.") 182 | return 183 | } 184 | 185 | for _, itemid := range c.Args() { 186 | 187 | itemid, err := strconv.Atoi(itemid) 188 | if err != nil { 189 | fmt.Println("item ID should be positive integer.") 190 | continue 191 | } 192 | 193 | // read item and print it, in case of misdeleteing 194 | item, err := notedb.ReadNoteItem(notedb.CurrentNote, itemid) 195 | if err != nil { 196 | fmt.Println(err) 197 | return 198 | } 199 | 200 | err = notedb.RemoveNoteItem(notedb.CurrentNote, itemid) 201 | if err != nil { 202 | fmt.Println(err) 203 | continue 204 | } 205 | 206 | fmt.Println(item) 207 | } 208 | } 209 | 210 | func funTag(c *cli.Context) { 211 | // list all tags 212 | note, err := notedb.GetCurrentNote() 213 | if err != nil { 214 | fmt.Println(err) 215 | return 216 | } 217 | 218 | if len(c.Args()) == 0 { 219 | tagstats := make([]TagStat, 0) 220 | for tag, taginfo := range note.Tags { 221 | tagstats = append(tagstats, TagStat{tag, len(taginfo)}) 222 | } 223 | sort.Sort(SortTagsByAmount(tagstats)) 224 | for _, tagstat := range tagstats { 225 | fmt.Printf("tag: %s\t(#. of items: %d).\n", tagstat.Tag, tagstat.Amount) 226 | } 227 | return 228 | } 229 | 230 | items, err := notedb.ItemByTag(c.Args()) 231 | if err != nil { 232 | fmt.Println(err) 233 | return 234 | } 235 | 236 | for _, item := range items { 237 | fmt.Println(item) 238 | } 239 | } 240 | 241 | func funSearch(c *cli.Context) { 242 | if len(c.Args()) == 0 { 243 | fmt.Println("search keyword needed.") 244 | return 245 | } 246 | 247 | items, err := notedb.ItemByRegexp(c.Args()) 248 | if err != nil { 249 | fmt.Println(err) 250 | return 251 | } 252 | 253 | for _, item := range items { 254 | fmt.Println(item) 255 | } 256 | } 257 | 258 | func funDump(c *cli.Context) { 259 | if len(c.Args()) > 0 { 260 | fmt.Println("no arguments should be given.") 261 | return 262 | } 263 | 264 | err := notedb.Dump() 265 | if err != nil { 266 | fmt.Println(err) 267 | return 268 | } 269 | } 270 | 271 | func funWipe(c *cli.Context) { 272 | if len(c.Args()) > 0 { 273 | fmt.Println("no arguments should be given.") 274 | return 275 | } 276 | 277 | reply, err := request_reply( 278 | "========================================\n"+ 279 | " Attention, it will clear all the data.\n"+ 280 | "========================================\n"+ 281 | " Type \"%s\" to continue:", 282 | "yes") 283 | if err != nil { 284 | fmt.Println(err) 285 | return 286 | } 287 | 288 | if reply == false { 289 | return 290 | } 291 | 292 | err = notedb.Wipe() 293 | if err != nil { 294 | fmt.Println(err) 295 | return 296 | } 297 | } 298 | 299 | func funRestore(c *cli.Context) { 300 | if len(c.Args()) != 1 { 301 | fmt.Println("dumpped filename needed.") 302 | return 303 | } 304 | 305 | reply, err := request_reply( 306 | "========================================\n"+ 307 | " Attention, it will clear all the data.\n"+ 308 | "========================================\n"+ 309 | " Type \"%s\" to continue:", 310 | "yes") 311 | if err != nil { 312 | fmt.Println(err) 313 | return 314 | } 315 | 316 | if reply == false { 317 | return 318 | } 319 | 320 | err = notedb.Restore(c.Args().First()) 321 | if err != nil { 322 | fmt.Println(err) 323 | return 324 | } 325 | } 326 | 327 | func funImport(c *cli.Context) { 328 | if len(c.Args()) != 3 { 329 | fmt.Println("three arguments needed: " + 330 | " .") 331 | return 332 | } 333 | notename, othernotename, filename := c.Args()[0], c.Args()[1], c.Args()[2] 334 | n, err := notedb.Import(notename, othernotename, filename) 335 | if err != nil { 336 | fmt.Println(err) 337 | return 338 | } 339 | fmt.Printf("%d items imported into note \"%s\".\n", n, notename) 340 | } 341 | 342 | func main() { 343 | notedb = NewNoteDB(DBFILE) 344 | defer notedb.Close() 345 | 346 | app := cli.NewApp() 347 | app.Name = "cnote" 348 | app.Usage = "A platform independent command line note app. https://github.com/shenwei356/cnote" 349 | app.Version = "1.2 (2014-07-22)" 350 | app.Author = "Wei Shen" 351 | app.Email = "shenwei356@gmail.com" 352 | 353 | app.Commands = []cli.Command{ 354 | { 355 | Name: "new", 356 | Usage: "Create a new note", 357 | Action: getFunc(funcs, "new"), 358 | }, 359 | { 360 | Name: "del", 361 | Usage: "Delete a note", 362 | Action: getFunc(funcs, "del"), 363 | }, 364 | { 365 | Name: "use", 366 | Usage: "Select a note", 367 | Action: getFunc(funcs, "use"), 368 | }, 369 | { 370 | Name: "list", 371 | ShortName: "ls", 372 | Usage: "List all notes", 373 | Action: getFunc(funcs, "list"), 374 | }, 375 | { 376 | Name: "add", 377 | Usage: "Add a note item", 378 | Action: getFunc(funcs, "add"), 379 | }, 380 | { 381 | Name: "rm", 382 | Usage: "Remove a note item", 383 | Action: getFunc(funcs, "rm"), 384 | }, 385 | { 386 | Name: "tag", 387 | ShortName: "t", 388 | Usage: "List items by tags. List all tags if no arguments given", 389 | Action: getFunc(funcs, "tag"), 390 | }, 391 | { 392 | Name: "search", 393 | ShortName: "s", 394 | Usage: "Search items with regular expression", 395 | Action: getFunc(funcs, "search"), 396 | }, 397 | { 398 | Name: "dump", 399 | Usage: "Dump whole database, for backup or transfer", 400 | Action: getFunc(funcs, "dump"), 401 | }, 402 | { 403 | Name: "wipe", 404 | Usage: "Attention! Wipe whole database", 405 | Action: getFunc(funcs, "wipe"), 406 | }, 407 | { 408 | Name: "restore", 409 | Usage: "Wipe whole database, and restore from dumpped file", 410 | Action: getFunc(funcs, "restore"), 411 | }, 412 | { 413 | Name: "import", 414 | Usage: "Import note items from dumpped data", 415 | Action: getFunc(funcs, "import"), 416 | }, 417 | } 418 | 419 | app.Run(os.Args) 420 | } 421 | -------------------------------------------------------------------------------- /sort.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type TagStat struct { 4 | Tag string 5 | Amount int 6 | } 7 | 8 | type SortTagsByAmount []TagStat 9 | 10 | func (tags SortTagsByAmount) Len() int { 11 | return len(tags) 12 | } 13 | 14 | func (tags SortTagsByAmount) Swap(i, j int) { 15 | tags[i], tags[j] = tags[j], tags[i] 16 | } 17 | 18 | func (tags SortTagsByAmount) Less(i, j int) bool { 19 | return tags[i].Amount > tags[j].Amount 20 | } 21 | 22 | type SortItemsById []Item 23 | 24 | func (items SortItemsById) Len() int { 25 | return len(items) 26 | } 27 | func (items SortItemsById) Swap(i, j int) { 28 | items[i], items[j] = items[j], items[i] 29 | } 30 | 31 | func (items SortItemsById) Less(i, j int) bool { 32 | return items[i].ItemID < items[j].ItemID 33 | } 34 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "os" 7 | "regexp" 8 | ) 9 | 10 | func trim_prefix(p, s string) string { 11 | return regexp.MustCompile("^"+p).ReplaceAllString(s, "") 12 | } 13 | 14 | func request_reply(message, reply string) (bool, error) { 15 | fmt.Printf(message, reply) 16 | 17 | reader := bufio.NewReader(os.Stdin) 18 | str, err := reader.ReadString('\n') 19 | if err != nil { 20 | return false, err 21 | } 22 | 23 | str = regexp.MustCompile(`[\r\n]`).ReplaceAllString(str, "") 24 | str = regexp.MustCompile(`^\s+|\s+$`).ReplaceAllString(str, "") 25 | if str != "yes" { 26 | fmt.Println("\ngiven up.") 27 | return false, nil 28 | } 29 | 30 | return true, nil 31 | } 32 | --------------------------------------------------------------------------------