diff --git a/tui/constants/const.go b/constants.go similarity index 72% rename from tui/constants/const.go rename to constants.go index 0746bc8..f6c98be 100644 --- a/tui/constants/const.go +++ b/constants.go @@ -1,26 +1,10 @@ -package constants +package main import ( - "github.com/bashbunni/pjs/entry" - "github.com/bashbunni/pjs/project" "github.com/charmbracelet/bubbles/key" - tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) -/* CONSTANTS */ - -var ( - // P the current tea program - P *tea.Program - // Er the entry repository for the tui - Er *entry.GormRepository - // Pr the project repository for the tui - Pr *project.GormRepository - // WindowSize store the size of the terminal window - WindowSize tea.WindowSizeMsg -) - /* STYLING */ // DocStyle styling for viewports @@ -42,13 +26,14 @@ type keymap struct { Delete key.Binding Back key.Binding Quit key.Binding + Edit key.Binding } // Keymap reusable key mappings shared across models var Keymap = keymap{ Create: key.NewBinding( - key.WithKeys("c"), - key.WithHelp("c", "create"), + key.WithKeys("n"), + key.WithHelp("n", "new"), ), Enter: key.NewBinding( key.WithKeys("enter"), @@ -70,4 +55,8 @@ var Keymap = keymap{ key.WithKeys("ctrl+c", "q"), key.WithHelp("ctrl+c/q", "quit"), ), + Edit: key.NewBinding( + key.WithKeys("e"), + key.WithHelp("e", "open in editor"), + ), } diff --git a/entry.go b/entry.go new file mode 100644 index 0000000..53e5015 --- /dev/null +++ b/entry.go @@ -0,0 +1,149 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "time" + + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/paginator" + "github.com/charmbracelet/bubbles/viewport" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/glamour" + "github.com/charmbracelet/lipgloss" +) + +const defaultEditor = "vi" + +type Entry struct { + path string + viewport viewport.Model + paginator paginator.Model + entries []string +} + +func InitEntry(path string) Entry { + vp := viewport.New(WindowSize.Width, WindowSize.Height) + e := getEntries(path) + p := paginator.New() + p.SetTotalPages(len(e)) + entry := Entry{ + path, + vp, + p, + e, + } + entry.setViewportContent() + return entry +} + +func openEditorCmd(path string) tea.Cmd { + editor := os.Getenv("EDITOR") + if editor == "" { + editor = defaultEditor + } + c := exec.Command(editor, path) + return tea.ExecProcess(c, func(err error) tea.Msg { + // TODO: return the file contents to update viewport content + contents, _ := os.ReadFile(path) + return editorFinishedMsg{err, contents} + }) +} + +// NewFilePath creates a markdown file to be opened in the editor +func NewFilePath(path string) (filepath string) { + today := time.Now().Format("2006-01-02") + filepath = fmt.Sprintf("%s/%s.md", path, today) + return filepath +} + +// ReadFile returns the contents of the file as a string +func ReadFile(path string) (string, error) { + out, err := os.ReadFile(path) + if err != nil { + return "", fmt.Errorf("%w: unable to read file: %s", err, path) + } + return string(out), nil +} + +func getEntries(path string) []string { + var entries []string + de, err := os.ReadDir(path) + if err != nil { + fmt.Errorf("unable to read dir: %w", err) + } + + for _, entry := range de { + if !entry.IsDir() { + entries = append(entries, entry.Name()) + } + } + return entries +} + +/* tea model interface */ + +// TODO: get num entries for paginator +// TODO: load entries as needed + +// Init get first entry +func (m Entry) Init() tea.Cmd { + return nil +} + +func (m Entry) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // TODO: have main model handle resizes and quits + var cmd tea.Cmd + var cmds []tea.Cmd + switch msg := msg.(type) { + case tea.WindowSizeMsg: + WindowSize.Width = msg.Width + WindowSize.Height = msg.Height + case editorFinishedMsg: + m.setViewportContent() + case tea.KeyMsg: + switch { + case key.Matches(msg, Keymap.Quit): + return m, tea.Quit + case key.Matches(msg, Keymap.Back): + // TODO: don't re-init list each time + return InitModel(), nil + + case key.Matches(msg, Keymap.Create): + cmds = append(cmds, openEditorCmd(NewFilePath(m.path))) + case key.Matches(msg, Keymap.Edit): + if len(m.entries) == 0 { + cmds = append(cmds, openEditorCmd(NewFilePath(m.path))) + } else { + cmds = append(cmds, openEditorCmd(m.currentFile())) + } + } + } + m.viewport, cmd = m.viewport.Update(msg) + cmds = append(cmds, cmd) + m.paginator, cmd = m.paginator.Update(msg) + cmds = append(cmds, cmd) + m.setViewportContent() // refresh the content on every Update call + return m, tea.Batch(cmds...) +} + +func (m Entry) View() string { + return lipgloss.JoinVertical(lipgloss.Left, m.viewport.View(), m.paginator.View()) +} + +func (m *Entry) setViewportContent() { + var content string + if len(m.entries) == 0 { + content = "There are no entries for this project :)" + } else { + file := m.currentFile() + content, _ = ReadFile(file) + } + str, _ := glamour.Render(content, "dark") + m.viewport.SetContent(str) +} + +func (m *Entry) currentFile() string { + return fmt.Sprintf("%s/%s", m.path, m.entries[m.paginator.Page]) +} diff --git a/entry/entry.go b/entry/entry.go deleted file mode 100644 index 43861c8..0000000 --- a/entry/entry.go +++ /dev/null @@ -1,52 +0,0 @@ -package entry - -import ( - "gorm.io/gorm" -) - -// Entry the entry model -type Entry struct { - gorm.Model - ProjectID uint `gorm:"foreignKey:Project"` - Message string -} - -// Repository the CRUD functionality for entries -type Repository interface { - DeleteEntryByID(entryID uint) error - DeleteEntries(projectID uint) error - GetEntriesByProjectID(projectID uint) ([]Entry, error) - CreateEntry(message []byte, projectID uint) error -} - -// GormRepository holds the gorm DB and is a EntryRepository -type GormRepository struct { - DB *gorm.DB -} - -// DeleteEntryByID delete an entry by its ID -func (g *GormRepository) DeleteEntryByID(entryID uint) error { - result := g.DB.Delete(&Entry{}, entryID) - return result.Error -} - -// TODO: unused -// DeleteEntries delete all entries for a given project -func (g *GormRepository) DeleteEntries(projectID uint) error { - result := g.DB.Where("project_id = ?", projectID).Delete(&Entry{}) - return result.Error -} - -// GetEntriesByProjectID get all entries for a given project -func (g *GormRepository) GetEntriesByProjectID(projectID uint) ([]Entry, error) { - var Entries []Entry - result := g.DB.Where("project_id = ?", projectID).Find(&Entries) - return Entries, result.Error -} - -// CreateEntry create a new entry in the database -func (g *GormRepository) CreateEntry(message []byte, projectID uint) error { - entry := Entry{Message: string(message[:]), ProjectID: projectID} - result := g.DB.Create(&entry) - return result.Error -} diff --git a/entry/entry_output.go b/entry/entry_output.go deleted file mode 100644 index 2f54eb8..0000000 --- a/entry/entry_output.go +++ /dev/null @@ -1,79 +0,0 @@ -package entry - -import ( - "fmt" - "os" - "os/exec" -) - -const divider = "---" - -// FormattedOutputFromEntries format all entries as a single string in reverse chronological order -func FormattedOutputFromEntries(Entries []Entry) []byte { - var output string - for i := len(Entries) - 1; i >= 0; i-- { - output += fmt.Sprintf("ID: %d\nCreated: %s\nMessage:\n\n %s\n %s\n", Entries[i].ID, Entries[i].CreatedAt.Format("2006-01-02"), Entries[i].Message, divider) - } - return []byte(output) -} - -// FormatEntry return the entry details as a formatted string -func FormatEntry(entry Entry) string { - return fmt.Sprintf("ID: %d\nCreated: %s\nMessage:\n\n %s\n %s\n", entry.ID, entry.CreatedAt.Format("2006-01-02"), entry.Message, divider) -} - -// ReverseList reverse the provided list -func ReverseList(list []Entry) []Entry { - var output []Entry - for i := len(list) - 1; i >= 0; i-- { - output = append(output, list[i]) - } - return output -} - -// OutputEntriesToMarkdown create an output file that contains the given entries in a formatted string -func OutputEntriesToMarkdown(entries []Entry) error { - file, err := os.OpenFile("./output.md", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) - if err != nil { - return fmt.Errorf("Cannot create file: %v", err) - } - defer func() { - err = file.Close() // want defer as close to acquisition of resources as possible - }() - output := FormattedOutputFromEntries(entries) - _, err = file.Write(output) - if err != nil { - return fmt.Errorf("Cannot save file: %v", err) - } - return err -} - -// OutputEntriesToPDF create a PDF from the given entries in their string format -func OutputEntriesToPDF(entries []Entry) error { - output := FormattedOutputFromEntries(entries) // []byte - pandoc := exec.Command("pandoc", "-s", "-o", "output.pdf") // c is going to run pandoc, so I'm assigning the pipe to c - wc, wcerr := pandoc.StdinPipe() // io.WriteCloser, err - if wcerr != nil { - return fmt.Errorf("Cannot stdin to pandoc: %v", wcerr) - } - goerr := make(chan error) - done := make(chan bool) - go func() { - var err error - defer func() { - err = wc.Close() - }() - _, err = wc.Write(output) - goerr <- err - close(goerr) - close(done) - }() - if err := <-goerr; err != nil { - return fmt.Errorf("Cannot write file to pandoc: %v", err) - } - err := pandoc.Run() - if err != nil { - return fmt.Errorf("Cannot run pandoc: %v", err) - } - return nil -} diff --git a/entry/entry_test.go b/entry/entry_test.go deleted file mode 100644 index 3e71c6c..0000000 --- a/entry/entry_test.go +++ /dev/null @@ -1,118 +0,0 @@ -package entry - -import ( - "fmt" - "testing" - - "gorm.io/driver/sqlite" - "gorm.io/gorm" -) - -func Setup(t *testing.T) (*gorm.DB, error) { - t.Helper() // allows me to log Gorm errors later - db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) - if err != nil { - return db, fmt.Errorf("unable to open in-memory SQLite DB: %w", err) - } - db.AutoMigrate(&Entry{}) - t.Cleanup(func() { - db.Migrator().DropTable(&Entry{}) - }) - return db, nil -} - -// DeleteEntryByID -func TestDeleteEntryForEmptyDB(t *testing.T) { - db, err := Setup(t) - if err != nil { - t.Fatal(err) - } - er := GormRepository{DB: db} - - er.DeleteEntryByID(1) - if err := db.Unscoped().Where("ID = 1").First(&Entry{}).Error; err == nil { - t.Error("expected error") - } -} - -func TestDeleteEntryWithTwoEntries(t *testing.T) { - db, err := Setup(t) - if err != nil { - t.Fatal(err) - } - er := GormRepository{DB: db} - - er.CreateEntry([]byte("hello world"), 1) - er.CreateEntry([]byte("I am just a world"), 1) - - er.DeleteEntryByID(1) - if err := db.Unscoped().Where("ID = 1").First(&Entry{}).Error; err != nil { - t.Error("expected no error") - } -} - -// DeleteEntries -func TestDeleteEntriesForEmptyDB(t *testing.T) { - db, err := Setup(t) - if err != nil { - t.Fatal(err) - } - - er := GormRepository{DB: db} - - er.DeleteEntries(1) - if err := db.Unscoped().Where("ID = 1").First(&Entry{}).Error; err == nil { - t.Error("expected error") - } -} - -func TestDeleteEntriesWithTwoEntries(t *testing.T) { - db, err := Setup(t) - if err != nil { - t.Fatal(err) - } - - er := GormRepository{DB: db} - - er.CreateEntry([]byte("hello world"), 1) - er.CreateEntry([]byte("I am just a world"), 1) - - er.DeleteEntries(1) - if err := db.Unscoped().Where("ID = 1").First(&Entry{}).Error; err != nil { - t.Error("expected no error") - } -} - -// GetEntriesByProjectID -func TestGetEntriesByProjectIDForEmptyDB(t *testing.T) { - db, err := Setup(t) - if err != nil { - t.Fatal(err) - } - - er := GormRepository{DB: db} - - got, _ := er.GetEntriesByProjectID(1) - if len(got) != 0 { - t.Error("expected an empty list of entries") - } -} - -func TestGetEntriesByProjectIDWithTwoEntries(t *testing.T) { - db, err := Setup(t) - if err != nil { - t.Fatal(err) - } - - er := GormRepository{DB: db} - - er.CreateEntry([]byte("hello world"), 1) - er.CreateEntry([]byte("I am just a world"), 1) - - got, _ := er.GetEntriesByProjectID(1) - if len(got) == 0 { - t.Error("expected a list with entries") - } -} - -// CreateEntry -> covered in previous tests diff --git a/go.mod b/go.mod index 54fe895..74d334d 100644 --- a/go.mod +++ b/go.mod @@ -3,41 +3,37 @@ module github.com/bashbunni/pjs go 1.17 require ( - github.com/charmbracelet/bubbles v0.14.0 - github.com/charmbracelet/bubbletea v0.22.1 + github.com/charmbracelet/bubbles v0.16.1 + github.com/charmbracelet/bubbletea v0.24.3-0.20230724163731-91dd12007337 github.com/charmbracelet/glamour v0.5.0 - github.com/charmbracelet/lipgloss v0.6.0 - gorm.io/driver/sqlite v1.4.3 - gorm.io/gorm v1.24.0 + github.com/charmbracelet/lipgloss v0.7.1 ) require ( github.com/alecthomas/chroma v0.10.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect - github.com/aymanbagabas/go-osc52 v1.2.1 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/aymerick/douceur v0.2.0 // indirect - github.com/containerd/console v1.0.3 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/dlclark/regexp2 v1.7.0 // indirect github.com/gorilla/css v1.0.0 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-localereader v0.0.1 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect - github.com/mattn/go-sqlite3 v1.14.16 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/microcosm-cc/bluemonday v1.0.21 // indirect - github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.13.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect - github.com/rivo/uniseg v0.4.2 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/sahilm/fuzzy v0.1.0 // indirect github.com/yuin/goldmark v1.5.2 // indirect github.com/yuin/goldmark-emoji v1.0.1 // indirect golang.org/x/net v0.1.0 // indirect - golang.org/x/sys v0.1.0 // indirect - golang.org/x/term v0.1.0 // indirect - golang.org/x/text v0.4.0 // indirect + golang.org/x/sync v0.3.0 // indirect + golang.org/x/sys v0.11.0 // indirect + golang.org/x/term v0.11.0 // indirect + golang.org/x/text v0.12.0 // indirect ) diff --git a/go.sum b/go.sum index d128555..fed531f 100644 --- a/go.sum +++ b/go.sum @@ -2,24 +2,22 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aymanbagabas/go-osc52 v1.0.3/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= -github.com/aymanbagabas/go-osc52 v1.2.1 h1:q2sWUyDcozPLcLabEMd+a+7Ea2DitxZVN9hTxab9L4E= -github.com/aymanbagabas/go-osc52 v1.2.1/go.mod h1:zT8H+Rk4VSabYN90pWyugflM3ZhpTZNC7cASDfUCdT4= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= -github.com/charmbracelet/bubbles v0.14.0 h1:DJfCwnARfWjZLvMglhSQzo76UZ2gucuHPy9jLWX45Og= -github.com/charmbracelet/bubbles v0.14.0/go.mod h1:bbeTiXwPww4M031aGi8UK2HT9RDWoiNibae+1yCMtcc= -github.com/charmbracelet/bubbletea v0.21.0/go.mod h1:GgmJMec61d08zXsOhqRC/AiOx4K4pmz+VIcRIm1FKr4= -github.com/charmbracelet/bubbletea v0.22.1 h1:z66q0LWdJNOWEH9zadiAIXp2GN1AWrwNXU8obVY9X24= -github.com/charmbracelet/bubbletea v0.22.1/go.mod h1:8/7hVvbPN6ZZPkczLiB8YpLkLJ0n7DMho5Wvfd2X1C0= +github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= +github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/bubbletea v0.24.3-0.20230724163731-91dd12007337 h1:k0PGZqmFg5sgqaSnFnoqCZKDfgmzSLjgMisF0yET3NA= +github.com/charmbracelet/bubbletea v0.24.3-0.20230724163731-91dd12007337/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= github.com/charmbracelet/glamour v0.5.0 h1:wu15ykPdB7X6chxugG/NNfDUbyyrCLV9XBalj5wdu3g= github.com/charmbracelet/glamour v0.5.0/go.mod h1:9ZRtG19AUIzcTm7FGLGbq3D5WKQ5UyZBbQsMQN0XIqc= -github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= -github.com/charmbracelet/lipgloss v0.5.0/go.mod h1:EZLha/HbzEt7cYqdFPovlqy5FZPj0xFhg5SaqxScmgs= -github.com/charmbracelet/lipgloss v0.6.0 h1:1StyZB9vBSOyuZxQUcUwGr17JmojPNm87inij9N3wJY= -github.com/charmbracelet/lipgloss v0.6.0/go.mod h1:tHh2wr34xcHjC2HCXIlGSG1jaDF0S0atAUvBMP6Ppuk= -github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw= -github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= +github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -28,57 +26,43 @@ github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= -github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/microcosm-cc/bluemonday v1.0.17/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= github.com/microcosm-cc/bluemonday v1.0.21 h1:dNH3e4PSyE4vNX+KlRGHT5KrSvjeUkoNPwEORjffHJg= github.com/microcosm-cc/bluemonday v1.0.21/go.mod h1:ytNkv4RrDrLJ2pqlsSI46O6IVXmZOBBD4SaJyDwwTkM= -github.com/muesli/ansi v0.0.0-20211018074035-2e021307bc4b/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= -github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA= -github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= -github.com/muesli/cancelreader v0.2.0/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= -github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.9.0/go.mod h1:R/LzAKf+suGs4IsO95y7+7DpFHO0KABgnZqtlyx2mBw= -github.com/muesli/termenv v0.11.1-0.20220204035834-5ac8409525e0/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.11.1-0.20220212125758-44cd13922739/go.mod h1:Bd5NYQ7pd+SrtBSrSNoBBmXlcY8+Xj4BMJgh8qcZrvs= -github.com/muesli/termenv v0.13.0 h1:wK20DRpJdDX8b7Ek2QfhvqhRQFZ237RGRO0RQ/Iqdy0= -github.com/muesli/termenv v0.13.0/go.mod h1:sP1+uffeLaEYpyOTb8pLCUctGcGLnoFjSn4YJK5e2bc= +github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= +github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= -github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -86,56 +70,37 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.4/go.mod h1:rmuwmfZ0+bvzB24eSC//bk1R1Zp3hM0OXYv/G2LIilg= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU= github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark-emoji v1.0.1 h1:ctuWEyzGBwiucEqxzwe0SOYDXPAucOrE9NQC18Wa1os= github.com/yuin/goldmark-emoji v1.0.1/go.mod h1:2w1E6FEWLcDQkoTE+7HU6QF1F6SLlNGjRIBbIZQFqkQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220204135822-1c1b9b1eba6a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0 h1:g6Z6vPFA9dYBAF7DWcH6sCcOntplXsDKcliusYijMlw= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/term v0.8.0 h1:n5xxQn2i3PC0yLAbjTpNT85q/Kgzcr2gIoX9OrJUols= +golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= +golang.org/x/term v0.11.0 h1:F9tnn/DA/Im8nCwm+fX+1/eBwi4qFjRT++MhtVC4ZX0= +golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= +golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/sqlite v1.4.3 h1:HBBcZSDnWi5BW3B3rwvVTc510KGkBkexlOg0QrmLUuU= -gorm.io/driver/sqlite v1.4.3/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= -gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74= -gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= diff --git a/main.go b/main.go index 35730c9..3f2889d 100644 --- a/main.go +++ b/main.go @@ -2,45 +2,110 @@ package main import ( "fmt" + "io/fs" "log" + "os" - "github.com/bashbunni/pjs/entry" - "github.com/bashbunni/pjs/project" - "github.com/bashbunni/pjs/tui" - "gorm.io/driver/sqlite" - "gorm.io/gorm" + tea "github.com/charmbracelet/bubbletea" ) -func openSqlite() (*gorm.DB, error) { - db, err := gorm.Open(sqlite.Open("new.db"), &gorm.Config{}) +// TODO: Defaults to $HOME/.pjs, can be changed by an env variable. +// TODO: have subdirectories named by project +// TODO: files named by date +// TODO: add flag for opening a specific project without opening list + +// TODO: this should probably only get called on program start... +// TODO: this could be named better... +func checkHome(home string) error { + var mkDirErr error + if _, err := os.Stat(home); err != nil { + mkDirErr = os.Mkdir(home, 0o755) + } + archived := fmt.Sprintf("%s/.archived", home) + if _, err := os.Stat(archived); err != nil { + mkDirErr = os.Mkdir(archived, 0o755) + } + return mkDirErr +} + +func defaultHome() (home string, err error) { + homeDir, err := os.UserHomeDir() if err != nil { - return db, fmt.Errorf("unable to open database: %w", err) + err = fmt.Errorf("No home directory found: %w", err) + return home, err } - err = db.AutoMigrate(&entry.Entry{}, &project.Project{}) + + home = fmt.Sprintf("%s/.pjs", homeDir) + err = checkHome(home) + return home, err +} + +// getProjects: get names of all directories in $HOME/.pjs +func getProjects() (projects []Project, err error) { + // TODO: handle error + home, _ := defaultHome() + var de []fs.DirEntry + de, err = os.ReadDir(home) if err != nil { - return db, fmt.Errorf("unable to migrate database: %w", err) + return projects, err + } + + for _, name := range de { + if name.Name() != ".archived" { + projects = append(projects, Project(name.Name())) + } } - return db, nil + return projects, err } func main() { - db, err := openSqlite() + var projects []Project + var home string + + // init home + home, err := defaultHome() if err != nil { log.Fatal(err) } - pr := project.GormRepository{DB: db} - er := entry.GormRepository{DB: db} - projects, err := pr.GetAllProjects() + + projects, err = getProjects() if err != nil { log.Fatal(err) } + if len(projects) < 1 { - name := project.NewProjectPrompt() - _, err := pr.CreateProject(name) + name := NewProjectPrompt() + if err := write(fmt.Sprintf("%s/%s", home, name)); err != nil { + log.Fatal(err) + } + } + + projects, err = getProjects() + if err != nil { + log.Fatal(err) + } + StartTea() +} + +func StartTea() { + if len(os.Getenv("PJ_DEBUG")) > 0 { + f, err := tea.LogToFile("debug.log", "debug") if err != nil { - log.Fatalf("error creating project: %v", err) + fmt.Println("fatal:", err) + os.Exit(1) } - } else { - tui.StartTea(pr, er) + defer f.Close() + } + + m := InitModel() + p := tea.NewProgram(m, tea.WithAltScreen()) + if err := p.Start(); err != nil { + log.Fatal(err) } } + +// Repository CRUD operations +type Repository interface { + Delete() + Rename() +} diff --git a/model.go b/model.go new file mode 100644 index 0000000..dfce9f1 --- /dev/null +++ b/model.go @@ -0,0 +1,217 @@ +package main + +import ( + "github.com/charmbracelet/bubbles/key" + "github.com/charmbracelet/bubbles/list" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" +) + +// TODO: rendering is broken; gets fixed when you resize...?! +type ( + SyncProjects struct{} + editorFinishedMsg struct { + err error + contents []byte + } + errMsg error + mode int +) + +const ( + nav mode = iota + edit + create +) + +var WindowSize struct { + Height int + Width int +} + +// Model the entryui model definition +type Model struct { + mode mode + projects []Project + list list.Model + input textinput.Model + quitting bool + err error +} + +// InitProject initialize the projectui model for your program +func InitModel() tea.Model { + input := textinput.New() + input.Prompt = "$ " + input.Placeholder = "Project name..." + input.CharLimit = 250 + input.Width = 50 + + items, _ := newList() + m := Model{mode: nav, list: list.NewModel(items, list.NewDefaultDelegate(), 8, 8), input: input} + if WindowSize.Height != 0 { + top, right, bottom, left := DocStyle.GetMargin() + m.list.SetSize(WindowSize.Width-left-right, WindowSize.Height-top-bottom-1) + } + m.list.Title = "projects" + m.list.AdditionalShortHelpKeys = func() []key.Binding { + return []key.Binding{ + Keymap.Create, + Keymap.Rename, + Keymap.Delete, + Keymap.Back, + } + } + return m +} + +func newList() ([]list.Item, error) { + projects, err := getProjects() + return projectsToItems(projects), err +} + +// Init run any intial IO on program start +func (m Model) Init() tea.Cmd { + return nil +} + +// Update handle IO and commands +func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + var cmds []tea.Cmd + switch msg := msg.(type) { + case tea.WindowSizeMsg: + WindowSize.Width = msg.Width + WindowSize.Height = msg.Height + top, right, bottom, left := DocStyle.GetMargin() + m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom-1) + case errMsg: + m.err = msg + case SyncProjects: + items, _ := newList() + m.mode = nav + m.input.Blur() + return m, m.list.SetItems(items) + case tea.KeyMsg: + switch m.mode { + case nav: + return m.handleNav(msg) + case edit: + if key.Matches(msg, Keymap.Enter) { + return m, renameProjectCmd( + Project(m.list.SelectedItem().FilterValue()), + m.input.Value()) + } + case create: + if key.Matches(msg, Keymap.Enter) { + return m, createProjectCmd(m.input.Value()) + } + } + // keys no matter the state + if key.Matches(msg, Keymap.Back) { + m.input.SetValue("") + m.input.Blur() + m.mode = nav + } + if key.Matches(msg, Keymap.Quit) { + m.quitting = true + return m, tea.Quit + } + + // only log keypresses for the input field when it's focused + m.input, cmd = m.input.Update(msg) + cmds = append(cmds, cmd) + } + return m, tea.Batch(cmds...) +} + +func (m Model) handleNav(msg tea.KeyMsg) (tea.Model, tea.Cmd) { + currentProject := m.list.SelectedItem().FilterValue() + switch { + case key.Matches(msg, Keymap.Create): + m.mode = create + m.input.Focus() + return m, textinput.Blink + case key.Matches(msg, Keymap.Enter): + p := Project(currentProject) + e := InitEntry(p.Path()) + return e, e.Init() + case key.Matches(msg, Keymap.Rename): + m.mode = edit + m.input.Focus() + return m, textinput.Blink + case key.Matches(msg, Keymap.Delete): + return m, deleteProjectCmd(Project(currentProject)) + } + var cmd tea.Cmd + m.list, cmd = m.list.Update(msg) + return m, cmd +} + +// View return the text UI to be output to the terminal +func (m Model) View() string { + var err string + if m.quitting { + return "" + } + if m.err == nil { + err = "" + } else { + err = m.err.Error() + } + if m.mode == nav { + return DocStyle.Render( + lipgloss.JoinVertical( + lipgloss.Left, + m.list.View(), + err, + )) + } + return DocStyle.Render( + lipgloss.JoinVertical( + lipgloss.Left, + m.list.View(), + m.input.View(), + err, + )) +} + +// TODO: use generics +// projectsToItems convert []Project to []list.Item +func projectsToItems(projects []Project) []list.Item { + items := make([]list.Item, len(projects)) + for i, proj := range projects { + items[i] = list.Item(proj) + } + return items +} + +/* commands */ + +func createProjectCmd(name string) tea.Cmd { + return func() tea.Msg { + if _, err := NewProject(name); err != nil { + return errMsg(err) + } + return SyncProjects{} + } +} + +func renameProjectCmd(p Project, name string) tea.Cmd { + return func() tea.Msg { + if err := p.Rename(name); err != nil { + return errMsg(err) + } + return SyncProjects{} + } +} + +func deleteProjectCmd(p Project) tea.Cmd { + return func() tea.Msg { + if err := p.Delete(); err != nil { + return errMsg(err) + } + return SyncProjects{} + } +} diff --git a/project.go b/project.go new file mode 100644 index 0000000..0e99ae7 --- /dev/null +++ b/project.go @@ -0,0 +1,64 @@ +package main + +import ( + "bufio" + "fmt" + "os" + "path/filepath" +) + +// TODO: add a note about manually deleting projects + +// Project is a project you'd like to track notes for +type Project string + +func NewProject(name string) (Project, error) { + p := Project(name) + err := write(p.Path()) + return p, err +} + +// Path: returns the project path +func (p Project) Path() string { + pwd, _ := defaultHome() + return filepath.Join(pwd, string(p)) +} + +func write(path string) error { + if err := os.Mkdir(path, 0o755); err != nil { + return fmt.Errorf("unable to create new project: %w", err) + } + + if err := os.Mkdir(fmt.Sprintf("%s/.archived", path), 0o755); err != nil { + return fmt.Errorf("unable to create archived dir: %w", err) + } + + return nil +} + +// Delete: archives the project directory +func (p Project) Delete() error { + path, _ := defaultHome() + return os.Rename(p.Path(), fmt.Sprintf("%s/.archived/%s", path, string(p))) +} + +// Rename: rename project +func (p Project) Rename(name string) error { + path, _ := defaultHome() + return os.Rename(p.Path(), fmt.Sprintf("%s/%s", path, name)) +} + +// NewProjectPrompt create a new project from user input to console +func NewProjectPrompt() string { + var name string + fmt.Println("what would you like to name your project?") + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + name = scanner.Text() + return name +} + +/* implementing list.Item */ +func (p Project) Title() string { return string(p) } +func (p Project) Description() string { return "" } +func (p Project) FilterValue() string { return string(p) } diff --git a/project/project.go b/project/project.go deleted file mode 100644 index f711d56..0000000 --- a/project/project.go +++ /dev/null @@ -1,130 +0,0 @@ -package project - -import ( - "bufio" - "fmt" - "log" - "os" - - "gorm.io/gorm" -) - -const ( - format string = "%d : %s\n" -) - -// Project the project holds entries -type Project struct { - gorm.Model - Name string -} - -// NewProject create a new project instance. -// DeletedAt defaults to the zero value for time.Time. -func NewProject(id uint, name string) *Project { - return &Project{Name: name} -} - -// Implement list.Item for Bubbletea TUI - -// Title the project title to display in a list -func (p Project) Title() string { return p.Name } - -// Description the project description to display in a list -func (p Project) Description() string { return fmt.Sprintf("%d", p.ID) } - -// FilterValue choose what field to use for filtering in a Bubbletea list component -func (p Project) FilterValue() string { return p.Name } - -// Repository CRUD operations for Projects -type Repository interface { - PrintProjects() - HasProjects() bool - GetProjectByID(projectID uint) (Project, error) - GetAllProjects() ([]Project, error) - CreateProject(name string) (Project, error) - DeleteProject(projectID uint) error - RenameProject(projectID uint) error -} - -// GormRepository holds the gorm DB and is a ProjectRepository -type GormRepository struct { - DB *gorm.DB -} - -// GetProjectByID get a project by ID -func (g *GormRepository) GetProjectByID(projectID uint) (Project, error) { - var project Project - if err := g.DB.Where("id = ?", projectID).First(&project).Error; err != nil { - return project, fmt.Errorf("Cannot find project: %v", err) - } - return project, nil -} - -// PrintProjects print all projects to the console -func (g *GormRepository) PrintProjects() { - projects, err := g.GetAllProjects() - if err != nil { - log.Fatal(err) - } - for _, project := range projects { - fmt.Printf(format, project.ID, project.Name) - } -} - -// GetAllProjects retrieve all projects from the database -func (g *GormRepository) GetAllProjects() ([]Project, error) { - var projects []Project - if err := g.DB.Find(&projects).Error; err != nil { - return projects, fmt.Errorf("Table is empty: %v", err) - } - return projects, nil -} - -// HasProjects see if a database has any projects -func (g *GormRepository) HasProjects() bool { - if projects, _ := g.GetAllProjects(); len(projects) == 0 { - return false - } - return true -} - -// CreateProject add a new project to the database -func (g *GormRepository) CreateProject(name string) (Project, error) { - proj := Project{Name: name} - if err := g.DB.Create(&proj).Error; err != nil { - return proj, fmt.Errorf("Cannot create project: %v", err) - } - return proj, nil -} - -// DeleteProject delete a project by ID -func (g *GormRepository) DeleteProject(projectID uint) error { - if err := g.DB.Delete(&Project{}, projectID).Error; err != nil { - return fmt.Errorf("Cannot delete project: %v", err) - } - return nil -} - -// RenameProject rename an existing project -func (g *GormRepository) RenameProject(id uint, name string) error { - var newProject Project - if err := g.DB.Where("id = ?", id).First(&newProject).Error; err != nil { - return fmt.Errorf("Unable to rename project: %w", err) - } - newProject.Name = name - if err := g.DB.Save(&newProject).Error; err != nil { - return fmt.Errorf("Unable to save project: %w", err) - } - return nil -} - -// NewProjectPrompt create a new project from user input to console -func NewProjectPrompt() string { - var name string - fmt.Println("what would you like to name your project?") - scanner := bufio.NewScanner(os.Stdin) - scanner.Scan() - name = scanner.Text() - return name -} diff --git a/project/project_test.go b/project/project_test.go deleted file mode 100644 index 554548d..0000000 --- a/project/project_test.go +++ /dev/null @@ -1,121 +0,0 @@ -package project - -import ( - "reflect" - "testing" - - "gorm.io/driver/sqlite" - "gorm.io/gorm" -) - -func Setup(t *testing.T) *gorm.DB { - t.Helper() // allows me to log Gorm errors later - db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{}) - if err != nil { - t.Fatalf("unable to open in-memory SQLite DB: %v", err) - } - db.AutoMigrate(&Project{}) - t.Cleanup(func() { - db.Migrator().DropTable(&Project{}) - }) - return db -} - -// TestCreateProject - -func TestCreateProjectForEmptyDB(t *testing.T) { - db := Setup(t) - pr := GormRepository{DB: db} - - pr.CreateProject("hello") - pr.CreateProject("world") - - got, _ := pr.GetAllProjects() - want := []Project{{Name: "hello"}, {Name: "world"}} - for i := range want { - if got[i].Name != want[i].Name { - t.Errorf("got %s want %s", got[i].Name, want[i].Name) - } - } -} - -// TestHasProjects - -func TestHasNoProjectsForEmptyDB(t *testing.T) { - db := Setup(t) - pr := GormRepository{DB: db} - - got := pr.HasProjects() - want := false - if got != want { - t.Errorf("got %t want %t", got, want) - } -} - -func TestHasTwoProjects(t *testing.T) { - db := Setup(t) - pr := GormRepository{DB: db} - - pr.CreateProject("hello") - pr.CreateProject("world") - - got := pr.HasProjects() - want := true - if got != want { - t.Errorf("got %t want %t", got, want) - } -} - -// TestGetAllProjects - -func TestGetProjectsFromEmptyDB(t *testing.T) { - db := Setup(t) - pr := GormRepository{DB: db} - - got, _ := pr.GetAllProjects() - if len(got) != 0 { - t.Error("did not get an empty project list") - } -} - -func TestGetTwoProjects(t *testing.T) { - db := Setup(t) - pr := GormRepository{DB: db} - - pr.CreateProject("hello") - pr.CreateProject("world") - - got, _ := pr.GetAllProjects() - want := []Project{{Name: "hello"}, {Name: "world"}} - for i := range want { - if got[i].Name != want[i].Name { - t.Errorf("got %s want %s", got[i].Name, want[i].Name) - } - } -} - -// TestGetProjectByID - -func TestGetProjectFromEmptyDB(t *testing.T) { - db := Setup(t) - pr := GormRepository{DB: db} - - _, err := pr.GetProjectByID(1) - if err == nil { - t.Error("expected an error") - } -} - -func TestGetProjectFromNonEmptyDB(t *testing.T) { - db := Setup(t) - pr := GormRepository{DB: db} - - pr.CreateProject("hello") - pr.CreateProject("world") - - got, err := pr.GetProjectByID(1) - want := Project{Name: "hello"} - if err != nil || reflect.DeepEqual(got, want) { - t.Errorf("got %s want %s. err == %v", got.Name, want.Name, err) - } -} diff --git a/tui/commands.go b/tui/commands.go deleted file mode 100644 index bda189d..0000000 --- a/tui/commands.go +++ /dev/null @@ -1,87 +0,0 @@ -package tui - -import ( - "fmt" - "os" - "os/exec" - - "github.com/bashbunni/pjs/project" - "github.com/bashbunni/pjs/tui/constants" - "github.com/bashbunni/pjs/utils" - tea "github.com/charmbracelet/bubbletea" -) - -const defaultEditor = "vim" - -/* PROJECTS */ - -func openEditorCmd() tea.Cmd { - file, err := os.CreateTemp(os.TempDir(), "") - if err != nil { - return func() tea.Msg { - return errMsg{error: err} - } - } - editor := os.Getenv("EDITOR") - if editor == "" { - editor = defaultEditor - } - c := exec.Command(editor, file.Name()) - return tea.ExecProcess(c, func(err error) tea.Msg { - return editorFinishedMsg{err, file} - }) -} - -func (m Entry) createEntryCmd(file *os.File) tea.Cmd { - return func() tea.Msg { - input, err := utils.ReadFile(file) - if err != nil { - return errMsg{fmt.Errorf("cannot read file in createEntryCmd: %v", err)} - } - if err := constants.Er.CreateEntry(input, m.activeProjectID); err != nil { - return errMsg{fmt.Errorf("cannot create entry: %v", err)} - } - if err := os.Remove(file.Name()); err != nil { - return errMsg{fmt.Errorf("cannot remove file: %v", err)} - } - if closeErr := file.Close(); closeErr != nil { - return errMsg{fmt.Errorf("unable to close file: %v", err)} - } - return m.setupEntries() - } -} - -/* ENTRIES */ - -func createProjectCmd(name string, pr *project.GormRepository) tea.Cmd { - return func() tea.Msg { - _, err := pr.CreateProject(name) - if err != nil { - return errMsg{err} - } - return updateProjectListMsg{} - } -} - -func renameProjectCmd(id uint, pr *project.GormRepository, name string) tea.Cmd { - return func() tea.Msg { - pr.RenameProject(id, name) - projects, err := pr.GetAllProjects() - if err != nil { - return errMsg{err} - } - items := projectsToItems(projects) - - return renameProjectMsg(items) - } -} - -func deleteProjectCmd(id uint, pr *project.GormRepository) tea.Cmd { - return func() tea.Msg { - err := pr.DeleteProject(id) - if err != nil { - return errMsg{err} - } - return updateProjectListMsg{} - } -} diff --git a/tui/entry.go b/tui/entry.go deleted file mode 100644 index cca3f9c..0000000 --- a/tui/entry.go +++ /dev/null @@ -1,142 +0,0 @@ -package tui - -import ( - "fmt" - "os" - - "github.com/bashbunni/pjs/entry" - "github.com/bashbunni/pjs/tui/constants" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/paginator" - "github.com/charmbracelet/bubbles/viewport" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/glamour" - "github.com/charmbracelet/lipgloss" -) - -type ( - errMsg struct{ error } - // UpdatedEntries holds the new entries from DB - UpdatedEntries []entry.Entry -) - -type editorFinishedMsg struct { - err error - file *os.File -} - -var cmd tea.Cmd - -// Entry implements tea.Model -type Entry struct { - viewport viewport.Model - activeProjectID uint - error string - paginator paginator.Model - entries []entry.Entry - quitting bool -} - -// Init run any intial IO on program start -func (m Entry) Init() tea.Cmd { - return nil -} - -// InitEntry initialize the entryui model for your program -func InitEntry(er *entry.GormRepository, activeProjectID uint, p *tea.Program) *Entry { - m := Entry{activeProjectID: activeProjectID} - top, right, bottom, left := constants.DocStyle.GetMargin() - m.viewport = viewport.New(constants.WindowSize.Width-left-right, constants.WindowSize.Height-top-bottom-1) - m.viewport.Style = lipgloss.NewStyle().Align(lipgloss.Bottom) - - // init paginator - m.paginator = paginator.New() - m.paginator.Type = paginator.Dots - m.paginator.ActiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "235", Dark: "252"}).Render("•") - m.paginator.InactiveDot = lipgloss.NewStyle().Foreground(lipgloss.AdaptiveColor{Light: "250", Dark: "238"}).Render("•") - - m.entries = m.setupEntries().(UpdatedEntries) - m.paginator.SetTotalPages(len(m.entries)) - // set content - m.setViewportContent() - return &m -} - -func (m *Entry) setupEntries() tea.Msg { - var err error - var entries []entry.Entry - if entries, err = constants.Er.GetEntriesByProjectID(m.activeProjectID); err != nil { - return errMsg{fmt.Errorf("Cannot find project: %v", err)} - } - entries = entry.ReverseList(entries) - return UpdatedEntries(entries) -} - -func (m *Entry) setViewportContent() { - var content string - if len(m.entries) == 0 { - content = "There are no entries for this project :)" - } else { - content = entry.FormatEntry(m.entries[m.paginator.Page]) - } - str, err := glamour.Render(content, "dark") - if err != nil { - m.error = "could not render content with glamour" - } - m.viewport.SetContent(str) -} - -// Update handle IO and commands -func (m Entry) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmds []tea.Cmd - switch msg := msg.(type) { - case tea.WindowSizeMsg: - top, right, bottom, left := constants.DocStyle.GetMargin() - m.viewport = viewport.New(constants.WindowSize.Width-left-right, constants.WindowSize.Height-top-bottom-6) - case errMsg: - m.error = msg.Error() - case editorFinishedMsg: - m.quitting = false - if msg.err != nil { - return m, tea.Quit - } - cmds = append(cmds, m.createEntryCmd(msg.file)) - case UpdatedEntries: - m.entries = msg - m.paginator.SetTotalPages(len(m.entries)) - m.setViewportContent() - case tea.KeyMsg: - switch { - case key.Matches(msg, constants.Keymap.Create): - // TODO: remove m.quitting after bug in Bubble Tea (#431) is fixed - m.quitting = true - return m, openEditorCmd() - case key.Matches(msg, constants.Keymap.Back): - return InitProject() - case key.Matches(msg, constants.Keymap.Quit): - m.quitting = true - return m, tea.Quit - } - } - - m.viewport, cmd = m.viewport.Update(msg) - m.paginator, cmd = m.paginator.Update(msg) - cmds = append(cmds, cmd) - m.setViewportContent() // refresh the content on every Update call - return m, tea.Batch(cmds...) -} - -func (m Entry) helpView() string { - // TODO: use the keymaps to populate the help string - return constants.HelpStyle("\n ↑/↓: navigate • esc: back • c: create entry • d: delete entry • q: quit\n") -} - -// View return the text UI to be output to the terminal -func (m Entry) View() string { - if m.quitting { - return "" - } - - formatted := lipgloss.JoinVertical(lipgloss.Left, "\n", m.viewport.View(), m.helpView(), constants.ErrStyle(m.error), m.paginator.View()) - return constants.DocStyle.Render(formatted) -} diff --git a/tui/project.go b/tui/project.go deleted file mode 100644 index d60c55f..0000000 --- a/tui/project.go +++ /dev/null @@ -1,178 +0,0 @@ -package tui - -import ( - "fmt" - "log" - - "github.com/bashbunni/pjs/project" - "github.com/bashbunni/pjs/tui/constants" - "github.com/charmbracelet/bubbles/key" - "github.com/charmbracelet/bubbles/list" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" -) - -// TODO: fix GormRepository vs Repository -type ( - updateProjectListMsg struct{} - renameProjectMsg []list.Item -) - -// SelectMsg the message to change the view to the selected entry -type SelectMsg struct { - ActiveProjectID uint -} - -type mode int - -const ( - nav mode = iota - edit - create -) - -// Model the entryui model definition -type Model struct { - mode mode - list list.Model - input textinput.Model - quitting bool -} - -// InitProject initialize the projectui model for your program -func InitProject() (tea.Model, tea.Cmd) { - input := textinput.New() - input.Prompt = "$ " - input.Placeholder = "Project name..." - input.CharLimit = 250 - input.Width = 50 - - items, err := newProjectList(constants.Pr) - m := Model{mode: nav, list: list.NewModel(items, list.NewDefaultDelegate(), 8, 8), input: input} - if constants.WindowSize.Height != 0 { - top, right, bottom, left := constants.DocStyle.GetMargin() - m.list.SetSize(constants.WindowSize.Width-left-right, constants.WindowSize.Height-top-bottom-1) - } - m.list.Title = "projects" - m.list.AdditionalShortHelpKeys = func() []key.Binding { - return []key.Binding{ - constants.Keymap.Create, - constants.Keymap.Rename, - constants.Keymap.Delete, - constants.Keymap.Back, - } - } - return m, func() tea.Msg { return errMsg{err} } -} - -func newProjectList(pr *project.GormRepository) ([]list.Item, error) { - projects, err := pr.GetAllProjects() - if err != nil { - return nil, fmt.Errorf("cannot get all projects: %w", err) - } - return projectsToItems(projects), err -} - -// Init run any intial IO on program start -func (m Model) Init() tea.Cmd { - return nil -} - -// Update handle IO and commands -func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - var cmds []tea.Cmd - switch msg := msg.(type) { - case tea.WindowSizeMsg: - constants.WindowSize = msg - top, right, bottom, left := constants.DocStyle.GetMargin() - m.list.SetSize(msg.Width-left-right, msg.Height-top-bottom-1) - case updateProjectListMsg: - projects, err := constants.Pr.GetAllProjects() - if err != nil { - log.Fatal(err) - } - items := projectsToItems(projects) - m.list.SetItems(items) - m.mode = nav - case renameProjectMsg: - m.list.SetItems(msg) - m.mode = nav - case tea.KeyMsg: - if m.input.Focused() { - if key.Matches(msg, constants.Keymap.Enter) { - if m.mode == create { - cmds = append(cmds, createProjectCmd(m.input.Value(), constants.Pr)) - } - if m.mode == edit { - cmds = append(cmds, renameProjectCmd(m.getActiveProjectID(), constants.Pr, m.input.Value())) - } - m.input.SetValue("") - m.mode = nav - m.input.Blur() - } - if key.Matches(msg, constants.Keymap.Back) { - m.input.SetValue("") - m.mode = nav - m.input.Blur() - } - // only log keypresses for the input field when it's focused - m.input, cmd = m.input.Update(msg) - cmds = append(cmds, cmd) - } else { - switch { - case key.Matches(msg, constants.Keymap.Create): - m.mode = create - m.input.Focus() - cmd = textinput.Blink - case key.Matches(msg, constants.Keymap.Quit): - m.quitting = true - return m, tea.Quit - case key.Matches(msg, constants.Keymap.Enter): - activeProject := m.list.SelectedItem().(project.Project) - entry := InitEntry(constants.Er, activeProject.ID, constants.P) - return entry.Update(constants.WindowSize) - case key.Matches(msg, constants.Keymap.Rename): - m.mode = edit - m.input.Focus() - cmd = textinput.Blink - case key.Matches(msg, constants.Keymap.Delete): - items := m.list.Items() - if len(items) > 0 { - cmd = deleteProjectCmd(m.getActiveProjectID(), constants.Pr) - } - default: - m.list, cmd = m.list.Update(msg) - } - cmds = append(cmds, cmd) - } - } - return m, tea.Batch(cmds...) -} - -// View return the text UI to be output to the terminal -func (m Model) View() string { - if m.quitting { - return "" - } - if m.input.Focused() { - return constants.DocStyle.Render(m.list.View() + "\n" + m.input.View()) - } - return constants.DocStyle.Render(m.list.View() + "\n") -} - -// TODO: use generics -// projectsToItems convert []model.Project to []list.Item -func projectsToItems(projects []project.Project) []list.Item { - items := make([]list.Item, len(projects)) - for i, proj := range projects { - items[i] = list.Item(proj) - } - return items -} - -func (m Model) getActiveProjectID() uint { - items := m.list.Items() - activeItem := items[m.list.Index()] - return activeItem.(project.Project).ID -} diff --git a/tui/tui.go b/tui/tui.go deleted file mode 100644 index 03f785d..0000000 --- a/tui/tui.go +++ /dev/null @@ -1,37 +0,0 @@ -package tui - -import ( - "fmt" - "log" - "os" - - "github.com/bashbunni/pjs/entry" - "github.com/bashbunni/pjs/project" - "github.com/bashbunni/pjs/tui/constants" - tea "github.com/charmbracelet/bubbletea" -) - -// StartTea the entry point for the UI. Initializes the model. -func StartTea(pr project.GormRepository, er entry.GormRepository) error { - if f, err := tea.LogToFile("debug.log", "help"); err != nil { - fmt.Println("Couldn't open a file for logging:", err) - os.Exit(1) - } else { - defer func() { - err = f.Close() - if err != nil { - log.Fatal(err) - } - }() - } - constants.Pr = &pr - constants.Er = &er - - m, _ := InitProject() // TODO: can we acknowledge this error - constants.P = tea.NewProgram(m, tea.WithAltScreen()) - if err := constants.P.Start(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } - return nil -} diff --git a/utils/file-utils.go b/utils/file-utils.go deleted file mode 100644 index 4fbc4a4..0000000 --- a/utils/file-utils.go +++ /dev/null @@ -1,24 +0,0 @@ -package utils - -import ( - "fmt" - "os" -) - -// CreateTempFile creates a temporary file to be opened in the editor -func CreateTempFile() (*os.File, error) { - file, err := os.CreateTemp(os.TempDir(), "*") - if err != nil { - return file, fmt.Errorf("unable to create new file: %w", err) - } - return file, nil -} - -// ReadFile returns the contents of the temp file as a string of bytes -func ReadFile(file *os.File) ([]byte, error) { - bytes, err := os.ReadFile(file.Name()) - if err != nil { - return []byte(""), fmt.Errorf("%w: unable to read temp file: %s", err, file.Name()) - } - return bytes, nil -}