前幾天整了一個命令行待辦事項管理器:medum。注意到自己已經有一陣子沒發過文章了,正好今晚有空,把它拿出來隨便聊聊,順便水一篇 233((
名稱由來#
這個項目叫做medum
。因為它的開發目的是防止我忘記ddl
,所以從一開始,它就是作為一個備忘錄(memorandum)被設計的。我從memorandum這個單詞中取了前兩個和後三個字母進行拼接,於是medum
誕生了。
代碼結構#
具體的開發初衷已經寫在了README.md
裡,所以接下來就說說代碼結構吧。
下面是所有的代碼文件:
.
├── config
│ └── config.go
├── main.go
├── output
│ └── output.go
├── path
│ └── path.go
├── public
│ └── public.go
├── sqlite
│ └── sqlite.go
└── text
└── text.go
每個模塊的功能如下:
path.go
:負責獲取配置文件夾路徑,配置文件路徑和 sqlite 數據庫路徑。config.go
:負責讀取配置文件,並在配置文件不存在的情況下寫入默認配置。public.go
:存放公共的結構體,包括在多個文件進行引用的Configuration
(配置文件)、Event
(事項)。sqlite.go
:sqlite 數據庫的打開,插入,更新,查詢,刪除功能的實現。text.go
:存放該程序近乎所有的文本信息(包括報錯、提醒以及 sql 語句)。output.go
:將配置文件的顏色映射為函數,對彩色輸出函數的反射調用進行一層封裝。
設計思路#
-
首先從功能出發,既然要讀取文件,那麼獲取路徑是必須的,於是首先考慮實現
path.go
:// path.go func GetPath() string { //獲取文件夾路徑,路徑為~/.medum path, err := homedir.Dir() if err != nil { fmt.Printf(text.HomedirError, err) os.Exit(1) } return filepath.Join(path, ".medum") } // 後面獲取配置文件和數據的路徑很簡單,在文件夾路徑後加文件名就可以
-
有了路徑,接下來就是要到路徑中讀取文件啦,但文件讀入到哪兒呢?為了讓內容有處可去,應該先實現一個配置文件的結構體。考慮到配置文件自定義的是顏色,必定需要被輸出模塊讀取,所以將其分離放在
public.go
裡,接著再寫config.go
:// public.go type Configuration struct { NumberColor string EventColor string TimeColor string }
// config.go func ReadConfig() *public.Configuration { configPath := path.GetConfigPath() // 檢測配置文件是否存在,不存在則寫入默認配置 if _, err := os.Stat(configPath); err != nil && !os.IsExist(err) { writeInitConfig(configPath) } // ... decoder := json.NewDecoder(file) conf := new(public.Configuration) // 將文件讀取到conf中 err = decoder.Decode(conf) // ... return conf func writeInitConfig(configPath string) { // 首先看文件夾是否存在,如果不存在則新建 tmp := path.GetPath() if _, err := os.Stat(tmp); !os.IsExist(err) { os.Mkdir(tmp, os.FileMode(0777)) } // 接著新建配置文件 file, err := os.Create(configPath) // 創建默認配置 conf := public.Configuration{ NumberColor: "red", EventColor: "blue", TimeColor: "yellow", } // 寫入 encoder := json.NewEncoder(file) encoder.Encode(conf) }
-
實現了配置文件的讀取,緊接著就是數據的操作了,考慮實現
sqlite.go
,但為了方便修改,儘量將所有的文本內容與邏輯分離,需要先寫text.go
,但在這之前,還要先考慮好事件結構體的內容,所以最終順序是public.go
->text.go
->sqlite.go
:// public.go // 包括id,事件內容,開始結束日期,當前狀態 type Event struct { ID int EventContent string BeginDate int64 EndDate int64 IsEnd uint8 }
// text.go const ( // 創建表,其中id自增,event為事件名,開始日期結束日期用時間戳存儲。 // 狀態標記方面,一個事件應該有未開始,正在進行,結束三個狀態,故isEnd可取0,1,2,對應三種狀態 CreateTable = `create table if not exists eventList( id integer primary key autoincrement, event varchar(100) not null, beginDate unsigned bigint not null, endDate unsigned bigint not null, isEnd smallint default 1 not null );` // 插入一行 InsertRow = `insert into eventList (event,beginDate,endDate) values (?,?,?);` // 當前時間大於結束時間則標記超時 MarkOutdate = `update eventList set isEnd=2 where ?>endDate;` // 當前時間小於開始時間則標記未開始 MarkNotStart = `update eventList set isEnd=0 where ?<beginDate;` // 介於開始結束之間標記正在進行 MarkInProgress = `update eventList set isEnd=1 where ? between beginDate and endDate;` // 獲得所有的事件,按事件狀態,結束日期排序 QueryRow = `select * from eventList order by isEnd desc,endDate` // 刪除超時事件 DeleteOutDate = `delete from eventList where isEnd=2` // 對於完成了的事件,通過id刪除 DeleteID = `delete from eventList where id=?` )
// sqlite.go // 打開數據庫 func openSqliteDB() *sql.DB { db, err := sql.Open("sqlite3", path.GetDataPath()) if err != nil { fmt.Printf(text.OpenDBError, err) os.Exit(1) } db.Exec(text.CreateTable) return db } // 剩余函數邏輯類似,取其中之一舉例: func InsertSqliteDB(eventContent string, beginDate, endDate time.Time) error { // 打開數據庫 db := openSqliteDB() // 結束時關閉 defer db.Close() // 執行語句 _, err := db.Exec(text.InsertRow, eventContent, beginDate.Unix(), endDate.Unix()) return err }
-
接下來考慮實現
output.go
:// output.go // 支持的所有顏色(採用map[string]interface{}存儲) var funcs = map[string]interface{}{ "red": color.New(color.FgRed), "blue": color.New(color.FgBlue), "cyan": color.New(color.FgCyan), "green": color.New(color.FgGreen), "yellow": color.New(color.FgYellow), "magenta": color.New(color.FgMagenta), "white": color.New(color.FgWhite), "black": color.New(color.FgBlack), "hired": color.New(color.FgHiRed), "hiblue": color.New(color.FgHiBlue), "hicyan": color.New(color.FgHiCyan), "higreen": color.New(color.FgHiGreen), "hiyellow": color.New(color.FgHiYellow), "himagenta": color.New(color.FgHiMagenta), "hiwhite": color.New(color.FgHiWhite), "hiblack": color.New(color.FgHiBlack), } // 使用reflect包進行動態調用 func call(m map[string]interface{}, color string, params ...interface{}) { function := reflect.ValueOf(m[color]).MethodByName("Printf") in := make([]reflect.Value, len(params)) for index, param := range params { in[index] = reflect.ValueOf(param) } function.Call(in) } // 將call進行一層封裝,對外公開Call函數 func Call(color string, params ...interface{}) { call(funcs, color, params...) }
-
萬事俱備,最後只需要在主函數中完成調用邏輯即可(當然還需要補全一些錯誤輸出、給用戶的提示等等,不過那些已經很簡單了):
// main.go // 接受的命令行參數 var ( begin string end string name string remove bool done int ) func main() { app := &cli.App{ // 省略掉參數綁定等流程 Action: func(c *cli.Context) error { // 讀取配置文件 conf := *config.ReadConfig() if remove { // -r:首先標記過期事件,接著刪除之 sqlite.UpdateSqliteDB(text.MarkOutdate) err := sqlite.DeleteOutDate() if err != nil { fmt.Printf(text.DeleteOutdateError, err) os.Exit(1) } fmt.Println(text.DeleteOutdateSuccess) } else if done != 0 { // -d int: 直接刪除該id err := sqlite.DeleteID(done) if err != nil { fmt.Printf(text.DeleteIDError, err) os.Exit(1) } fmt.Println(text.DeleteIDSuccess) } else { if len(end) == 0 { // 如果沒有-d,打印事件列表 // 這裡只標記進行中和超時,是因為在插入事件時已經標記了是否未開始,時間順序流動,不可能從開始變為未開始 sqlite.UpdateSqliteDB(text.MarkInProgress) sqlite.UpdateSqliteDB(text.MarkOutdate) // ...省略輸出部分 } } else { // 如果存在-e,則解析結束時間,開始事件如果有就解析,沒有默認為當前時間 var beginTime, endTime time.Time endTime = handleString(end) if len(begin) == 0 { beginTime = time.Now() } else { beginTime = handleString(begin) } if beginTime.Unix() >= endTime.Unix() { fmt.Println(text.TimeError) os.Exit(1) } // 將輸入事件插入 err := sqlite.InsertSqliteDB(name, beginTime, endTime) if err != nil { fmt.Printf(text.InsertDBError, err) } // 標記未開始 sqlite.UpdateSqliteDB(text.MarkNotStart) fmt.Println(text.InsertSuccess) } } return nil }} err := app.Run(os.Args) if err != nil { log.Fatal(err) } } func handleString(tm string) time.Time { // 時間字符串轉time.Time,只接受month.day.hour.minute格式 tmp := strings.Split(tm, ".") // 長度不為4則退出 if len(tmp) != 4 { fmt.Println(text.LengthError) os.Exit(1) } else { // 補全0,例如將5.20.12.0補全為05.20.12.00,防止解析錯誤 for index := range tmp { if len(tmp[index]) == 1 { tmp[index] = "0" + tmp[index] } } stdString := fmt.Sprintf(text.MyTime, strconv.Itoa(time.Now().Year()), tmp[0], tmp[1], tmp[2], tmp[3]) result, err := time.ParseInLocation(text.StandardTime, stdString, time.Now().Local().Location()) if err != nil { fmt.Printf(text.ParamError, err) os.Exit(1) } return result } //useless line, just to prevent warning return time.Now() } func formatTime(begin, end int64, IsEnd uint8) string { // 將時間轉換成%s time remaining/%s time beginning格式,其中的%s調用下面的formatTimeString函數獲取 now := time.Now() if IsEnd == 0 { beginTime := time.Unix(begin, 0) dur := beginTime.Sub(now) return fmt.Sprintf(text.TimeBeginning, formatTimeString(dur.Minutes())) } else if IsEnd == 1 { endTime := time.Unix(end, 0) dur := endTime.Sub(now) return fmt.Sprintf(text.TimeRemaining, formatTimeString(dur.Minutes())) } else { return fmt.Sprintf(text.TimeRemaining, "no time") } } func formatTimeString(min float64) string { // 獲取天/小時/分鐘 var tmp string if min > 1440 { tmp = strconv.Itoa(int(min/1440)) + " days" } else if min > 60 { tmp = strconv.Itoa(int((min / 60))) + " hours" } else { tmp = strconv.Itoa(int(min)) + " minutes" } return tmp }
這樣下來,預想的功能就實現的差不多了,任務成功完成。
結束語#
okk,這樣就結束啦,算是寫了一個小總結吧。
代碼基本全程都有註釋(不過為了符合golang
的規範,使用的是塑料英語 XD),覺得自己代碼寫的還是蠻易讀的,歡迎大家閱讀給出建議哦!