init todo buttons, fix delTodo

This commit is contained in:
Casual 2024-07-02 12:17:21 +03:00
parent f4fe3f301d
commit 6a4650cfd2
6 changed files with 385 additions and 117 deletions

212
caldav.go
View File

@ -11,6 +11,9 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"time" "time"
"errors" "errors"
"net/http"
// "net/url"
// "fmt" // "fmt"
) )
@ -339,10 +342,9 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.
// //TODO UID:7ed30f40-fce1-422c-be3b-0486dcfe8943 // //TODO UID:7ed30f40-fce1-422c-be3b-0486dcfe8943
// //TODO RELATED-TO:7ed30f40-fce1-422c-be3b-0486dcfe8943 # subtask // //TODO RELATED-TO:7ed30f40-fce1-422c-be3b-0486dcfe8943 # subtask
// //TODO PRIORITY:1 #1-high, 5-mid, 9-low // //TODO PRIORITY:1 #1-high, 5-mid, 9-low
//TODO repeat function??? //TODO on complete -repeat function
//TODO repeate - RRULE:FREQ=DAILY;INTERVAL=1
//TODO on complete -repeat function // RRULE:FREQ=WEEKLY;INTERVAL=1
// RRULE:FREQ=WEEKLY;INTERVAL=1
//TODO if no repeat - mark as complted //TODO if no repeat - mark as complted
// STATUS:COMPLETED // STATUS:COMPLETED
@ -376,40 +378,88 @@ func ParseDueDateTODOs(calObjs []caldav.CalendarObject, date time.Time) ([]ical.
// type todoInterface struct { type TodoInterface struct {
// name string name string
// description string description string
// priority int priority int
// dueTime time.Time dueTime time.Time
// name string alarmOffset string
// } repeat string
//TODO repeat
//TODO subtasks
}
func CreateTodo(name,description string, priority int, dueTime time.Time) (event ical.Event,err error) { func CreateTodo(info TodoInterface) (event ical.Event,err error) {
uid, err := uuid.NewUUID() uid, err := uuid.NewUUID()
if err != nil {return} if err != nil {return}
event = *ical.NewEvent() event = *ical.NewEvent()
event.Name = ical.CompToDo //VTODO
event.Props.SetText(ical.PropUID, uid.String()) event.Props.SetText(ical.PropUID, uid.String())
event.Props.SetText(ical.PropSummary, name) event.Props.SetText(ical.PropSummary, info.name)
event.Props.SetText(ical.PropDescription, description) event.Props.SetText(ical.PropDescription, info.description)
if !dueTime.IsZero() {event.Props.SetDateTime(ical.PropDateTimeEnd, dueTime)} // 'zero' time is `time.Time{}` if !info.dueTime.IsZero() {event.Props.SetDateTime(ical.PropDue, info.dueTime)} // 'zero' time is `time.Time{}`
switch priority { //TODO add alarm
switch info.priority {
case 0: //No priority case 0: //No priority
event.Props.SetText(ical.PropPriority, "0") event.Props.SetText(ical.PropPriority, "0")
case 1: //Light case 1: //Light
event.Props.SetText(ical.PropPriority, "1") event.Props.SetText(ical.PropPriority, "9")
case 2: //Medium case 2: //Medium
event.Props.SetText(ical.PropPriority, "5") event.Props.SetText(ical.PropPriority, "5")
case 3: //Urgent case 3: //Urgent
event.Props.SetText(ical.PropPriority, "9") event.Props.SetText(ical.PropPriority, "1")
default: default:
err = errors.New("Wrong priority, expecting 0-3") err = errors.New("Wrong priority, expecting 0-3")
return return
} }
//TODO repeat - RRULE:FREQ=DAILY;INTERVAL=1
if info.alarmOffset != "" {
// alarmComponent := ical.Component{Name:ical.CompAlarm}
alarmComponent := ical.NewComponent(ical.CompAlarm)
alarmComponent.Props.SetText(ical.PropAction,"DISPLAY")
// alarmComponent.Props.Add(&ical.Prop{Name:ical.PropAction,Value:"DISPLAY",})
alarmComponent.Props.Add(&ical.Prop{Name:ical.PropDescription,Value:"Default Alarm Tempus description",})
alarmComponent.Props.SetText(ical.PropDescription,"Default Alarm Tempus description")
// ACTION:DISPLAY
// DESCRIPTION:Default Tasks.org description
var value string
if info.alarmOffset == "0" {
// TRIGGER;RELATED=END:PT0S
value = "PT0S"
}
if strings.HasSuffix(info.alarmOffset,"h") {
offset,_ := strings.CutSuffix(info.alarmOffset,"h")
value="-PT" + offset + "H"
// TRIGGER;RELATED=END:-PT1H
}
//TODO stop next if
if strings.HasSuffix(info.alarmOffset,"m") {
offset,_ := strings.CutSuffix(info.alarmOffset,"m")
value="-PT" + offset + "M"
// TRIGGER;RELATED=END:-PT10M
}
if strings.HasSuffix(info.alarmOffset,"d") {
offset,_ := strings.CutSuffix(info.alarmOffset,"d")
value="-P" + offset + "D"
// TRIGGER;RELATED=END:-P1D
}
alarmComponent.Props.SetText("TRIGGER;RELATED=END",value)
event.Children = append(event.Children,alarmComponent)
}
event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now().UTC()) event.Props.SetDateTime(ical.PropDateTimeStamp, time.Now().UTC())
event.Props.SetDateTime(ical.PropCreated, time.Now().UTC()) //TODO if it don't exist already (in case if we edit todo) event.Props.SetDateTime(ical.PropCreated, time.Now().UTC()) //TODO if it don't exist already (in case if we edit todo)
event.Props.SetDateTime(ical.PropLastModified, time.Now().UTC()) event.Props.SetDateTime(ical.PropLastModified, time.Now().UTC())
//TODO add a function to verify event (exist in ical lib)
return return
} }
@ -417,54 +467,102 @@ func CreateTodo(name,description string, priority int, dueTime time.Time) (event
func (m model) UploadTodo(event ical.Event) (err error) { func (m model) UploadTodo(event ical.Event) (err error) {
// event.Props.SetDateTime(ical.PropDateTimeStart, startDateTime) // event.Props.SetDateTime(ical.PropDateTimeStart, startDateTime)
//TODO PropPriority = "PRIORITY"
// TODO Alarm component properties // TODO Alarm component properties
// PropAction = "ACTION" // PropAction = "ACTION"
// PropRepeat = "REPEAT" // PropRepeat = "REPEAT"
// PropTrigger = "TRIGGER"} // PropTrigger = "TRIGGER"}
calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath) // calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath) //makes error on nextcloud
if err != nil {return} // if err != nil {return err}
calendar.Data.Component.Children = append(calendar.Data.Component.Children, event.Component)
var buf strings.Builder calendar := ical.NewCalendar()
encoder := ical.NewEncoder(&buf) calendar.Props.SetText(ical.PropProductID, "+//Casual//Tempus//EN")
err = encoder.Encode(calendar.Data) calendar.Props.SetText(ical.PropVersion, "2.0")
if err != nil {return} calendar.Component.Children = append(calendar.Component.Children, event.Component)
_, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath, calendar.Data)
if err != nil {return} todoGUID,err := event.Props.Get(ical.PropUID).Text()
return nil if err != nil {return err}
//TODO check GUID uniq and regenerate if needed
var buf strings.Builder
encoder := ical.NewEncoder(&buf)
err = encoder.Encode(calendar)
if err != nil {return err}
_, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath+todoGUID+".isc", calendar)
if err != nil {return err}
return nil
} }
func (m model) DelTodo(todo ical.Event) (err error) { func (m model) DelTodo(delUID string) (err error) {
// func (m model) DelTodo(todo ical.Event) (err error) {
delUID,err := todo.Props.Get(ical.PropUID).Text() // delUID,err := todo.Props.Get(ical.PropUID).Text()
if err != nil {return} // if err != nil {return}
calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath) // calendar, err := client.GetCalendarObject(ctx, m.Creds.CalendarPath+delUID+".isc")
if err != nil {return} // if err != nil {return}
//
var newEvents []*ical.Component // var newEvents []*ical.Component
for _, component := range calendar.Data.Component.Children { // for _, component := range calendar.Data.Component.Children {
if component.Name == ical.CompEvent { // if component.Name == ical.CompEvent {
var uid string // var uid string
uid, err = component.Props.Text(ical.PropUID) // uid, err = component.Props.Text(ical.PropUID)
if err != nil {return} // if err != nil {return}
if uid != delUID { // if uid != delUID {
newEvents = append(newEvents, component) // newEvents = append(newEvents, component)
} // }
} // }
} // }
//
calendar.Data.Component.Children = newEvents // calendar.Data.Component.Children = newEvents
var buf strings.Builder // var buf strings.Builder
encoder := ical.NewEncoder(&buf) // encoder := ical.NewEncoder(&buf)
err = encoder.Encode(calendar.Data) // err = encoder.Encode(calendar.Data)
if err != nil {return} // if err != nil {return}
//
_, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath, calendar.Data) // _, err = client.PutCalendarObject(ctx, m.Creds.CalendarPath, calendar.Data)
if err != nil {return} // if err != nil {return}
// req := http.Request{
// Method: "DELETE",
// URL: url.Parse(m.Creds.URL + m.Creds.CalendarPath + delUID + ".isc"),
//
// }
client := &http.Client{}
parts := strings.Split(m.Creds.URL, "/")
baseURL := parts[0]+"//"+parts[2]
// Create request
req, err := http.NewRequest("DELETE", baseURL + m.Creds.CalendarPath + delUID + ".isc", nil)
if err != nil {
// fmt.Println(err)
return
}
req.SetBasicAuth(m.Creds.Username, m.Creds.Password)
// Fetch Request
resp, err := client.Do(req)
if err != nil {
// fmt.Println(err)
return
}
defer resp.Body.Close()
// Read Response Body
// respBody, err := ioutil.ReadAll(resp.Body)
// if err != nil {
// // fmt.Println(err)
// return
// }
if resp.Status != "204 No Content" {return errors.New("Can't delete, response status: "+resp.Status+".")}
// Display Results
// fmt.Println("response Status : ", resp.Status)
// fmt.Println("response Headers : ", resp.Header)
// fmt.Println("response Body : ", string(respBody))
return nil return nil
} }
@ -472,8 +570,10 @@ func (m model) DelTodo(todo ical.Event) (err error) {
func (m model) EditTodo(todo ical.Event) (err error) { func (m model) EditTodo(todo ical.Event) (err error) {
//TODO is there proper edit function ??? //TODO is there proper edit function ???
uid,err := todo.Props.Get(ical.PropUID).Text()
if err != nil {return}
err = m.DelTodo(todo) err = m.DelTodo(uid)
if err != nil {return} if err != nil {return}
err = m.UploadTodo(todo) err = m.UploadTodo(todo)

13
main.go
View File

@ -3,6 +3,8 @@ package main
import ( import (
"fmt" "fmt"
"os" "os"
"net/http"
"crypto/tls"
// "sync" // "sync"
// "time" // "time"
@ -26,6 +28,7 @@ func errHandler(err error, message string) {
} }
func main() { func main() {
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
options, err := ParseOptions() options, err := ParseOptions()
errHandler(err, "Error parsing options") errHandler(err, "Error parsing options")
m := InitModel() m := InitModel()
@ -61,6 +64,11 @@ func main() {
os.Exit(1) os.Exit(1)
} }
m.LoginToCalendar()
m.GatherTodos()
// calendarObjects, err := GetTODOs(calPath) // calendarObjects, err := GetTODOs(calPath)
// errHandler(err, "Error getting TODOs") // errHandler(err, "Error getting TODOs")
// //
@ -142,7 +150,10 @@ func main() {
//DEBUG stuff //DEBUG stuff
// task, err := CreateTodo("testName","description",3,time.Now())
// errHandler(err,"test fail")
// err = m.UploadTodo(task)
// errHandler(err,"test fail2")
//DEBUG stuff //DEBUG stuff

View File

@ -1,6 +1,7 @@
package main package main
import ( import (
"os"
"errors" "errors"
"github.com/projectdiscovery/goflags" "github.com/projectdiscovery/goflags"
"sync" "sync"
@ -18,6 +19,8 @@ type Options struct {
User string User string
Password string Password string
Proxy string
} }
func ParseOptions() (*Options, error) { func ParseOptions() (*Options, error) {
@ -39,6 +42,7 @@ func ParseOptions() (*Options, error) {
flagSet.StringVarP(&options.User, "l", "login", "", "WebDAV login username"), flagSet.StringVarP(&options.User, "l", "login", "", "WebDAV login username"),
flagSet.StringVarP(&options.Password, "p", "password", "", "WebDAV password (forbid filesystem access in WebDAV and don't forget to clean shell history!)"), flagSet.StringVarP(&options.Password, "p", "password", "", "WebDAV password (forbid filesystem access in WebDAV and don't forget to clean shell history!)"),
flagSet.StringVarP(&options.Calendar, "c", "calendar", "", "CalDAV calendar (to-do list) name to use (works only with -u,-l,-p flags)"), flagSet.StringVarP(&options.Calendar, "c", "calendar", "", "CalDAV calendar (to-do list) name to use (works only with -u,-l,-p flags)"),
flagSet.StringVarP(&options.Proxy, "P", "proxy", "", "HTTP proxy to debug errors"),
// flagSet.BoolVarP(&options.SkipSave, "s", "no-save", false, "skip save to keyring"), //TODO // flagSet.BoolVarP(&options.SkipSave, "s", "no-save", false, "skip save to keyring"), //TODO
) )
@ -64,5 +68,8 @@ func (options *Options) SanityCheck() error {
} }
} }
if options.Proxy != "" {os.Setenv("HTTP_PROXY", options.Proxy)}
return nil return nil
} }

95
tui-todo-keys.go Normal file
View File

@ -0,0 +1,95 @@
package main
import (
// "github.com/emersion/go-ical"
// "time"
"github.com/charmbracelet/bubbles/list"
"github.com/charmbracelet/bubbles/key"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
// "strings"
// "errors"
)
var (
appStyle = lipgloss.NewStyle().Padding(1, 2)
// titleStyle = lipgloss.NewStyle().
// Foreground(lipgloss.Color("#FFFDF5")).
// Background(lipgloss.Color("#25A065")).
// Padding(0, 1)
statusMessageStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "#04B575", Dark: "#04B575"}).
Render
)
// buttons
func (m model) newItemDelegate(keys *delegateKeyMap) list.DefaultDelegate {
d := list.NewDefaultDelegate()
d.UpdateFunc = func(msg tea.Msg, ml *list.Model) tea.Cmd {
var title string
var todoUID string
if i, ok := ml.SelectedItem().(TODO); ok {
title = i.Title()
todoUID = i.UID()
} else {
return nil
}
switch msg := msg.(type) {
case tea.KeyMsg:
switch {
case key.Matches(msg, keys.choose):
return ml.NewStatusMessage(statusMessageStyle("You chose " + title))
case key.Matches(msg, keys.remove):
err := m.DelTodo(todoUID)
if err != nil {return errHandler_tui(err,"can't delete item")}
index := ml.Index()
ml.RemoveItem(index)
if len(ml.Items()) == 0 {
keys.remove.SetEnabled(false)
}
return ml.NewStatusMessage(statusMessageStyle("Deleted " + title))
}
}
return nil
}
help := []key.Binding{keys.choose, keys.remove}
d.ShortHelpFunc = func() []key.Binding {
return help
}
d.FullHelpFunc = func() [][]key.Binding {
return [][]key.Binding{help}
}
return d
}
type delegateKeyMap struct {
choose key.Binding
remove key.Binding
}
func newDelegateKeyMap() *delegateKeyMap {
return &delegateKeyMap{
choose: key.NewBinding(
key.WithKeys("enter"),
key.WithHelp("enter", "choose"),
),
remove: key.NewBinding(
key.WithKeys("x", "backspace"),
key.WithHelp("x", "delete"),
),
}
}

View File

@ -5,6 +5,8 @@ import (
"github.com/emersion/go-ical" "github.com/emersion/go-ical"
"time" "time"
"github.com/charmbracelet/bubbles/list" "github.com/charmbracelet/bubbles/list"
"strings"
"errors"
) )
@ -15,6 +17,11 @@ func (i TODO) Title() string {
if err != nil {return "<EMPTY>"} if err != nil {return "<EMPTY>"}
return out return out
} }
func (i TODO) UID() string {
out,err := i.Props.Get(ical.PropUID).Text()
if err != nil {return "<EMPTY>"}
return out
}
func (i TODO) Description() string { func (i TODO) Description() string {
out,err := i.Props.Get(ical.PropDescription).Text() out,err := i.Props.Get(ical.PropDescription).Text()
if err != nil {return ""} if err != nil {return ""}
@ -29,6 +36,9 @@ func (i TODO) FilterValue() string {
func (m *model) GatherTodos() (err error) { func (m *model) GatherTodos() (err error) {
//TODO more modular approach //TODO more modular approach
calendarObjects, err := GetTODOs(m.Creds.CalendarPath) calendarObjects, err := GetTODOs(m.Creds.CalendarPath)
@ -62,12 +72,32 @@ func (m *model) GatherTodos() (err error) {
itemsTomorrow = append(itemsTomorrow, todo) itemsTomorrow = append(itemsTomorrow, todo)
} }
m.TodayTab = list.New(itemsToday, list.NewDefaultDelegate(), 0, 0)
delegateKeys := newDelegateKeyMap()
delegate := m.newItemDelegate(delegateKeys)
// m.TodayTab = list.New(itemsToday, list.NewDefaultDelegate(), 0, 0)
m.TodayTab = list.New(itemsToday, delegate, 0, 0)
m.TodayTab.Title = "Today" m.TodayTab.Title = "Today"
m.TomorrowTab = list.New(itemsTomorrow, list.NewDefaultDelegate(), 0, 0) // m.TomorrowTab = list.New(itemsTomorrow, list.NewDefaultDelegate(), 0, 0)
m.TomorrowTab = list.New(itemsTomorrow, delegate, 0, 0)
m.TomorrowTab.Title = "Tomorrow" m.TomorrowTab.Title = "Tomorrow"
return nil
}
func (m *model) UpdateTodos(todo ical.Event) (err error) {
today := time.Now()
tomorrow := time.Now().AddDate(0, 0, 1)
errorI := 0
if strings.HasPrefix(todo.Props["DUE"][0].Value, today.Format("20060102")) {m.TodayTab.InsertItem(-1,TODO(todo))} else {errorI += 1}
if strings.HasPrefix(todo.Props["DUE"][0].Value, tomorrow.Format("20060102")) {m.TomorrowTab.InsertItem(-1,TODO(todo)) } else {errorI += 1}
if errorI == 2 {return errors.New("don't match today and tomorrow")}
return nil return nil
} }

139
tui.go
View File

@ -3,7 +3,7 @@ package main
import ( import (
// "fmt" // "fmt"
// "os" // "os"
// "time" "time"
// "io" // "io"
// "strings" // "strings"
@ -33,7 +33,20 @@ import (
type errMsg struct {message string} type errMsg struct {message string}
func (m model) errHandler(desc string,err error) (tea.Cmd) {
//TODO fix multiple errHandlers
//TODO rm me
func (m model) errHandler(err error,desc string) (tea.Cmd) {
if err != nil {
output := desc+": "+err.Error()
return func() tea.Msg { return errMsg{output} }
// return errMsg{output}
// m.Send(output)
}
return nil
}
func errHandler_tui(err error,desc string) (tea.Cmd) {
if err != nil { if err != nil {
output := desc+": "+err.Error() output := desc+": "+err.Error()
return func() tea.Msg { return errMsg{output} } return func() tea.Msg { return errMsg{output} }
@ -46,34 +59,46 @@ func (m model) errHandler(desc string,err error) (tea.Cmd) {
func (m model) Init() tea.Cmd { func (m model) Init() tea.Cmd {
return textinput.Blink return textinput.Blink
// return nil // return nil
} }
//TODO add changing calendar
func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
//TODO separate to funcs //TODO separate to funcs
var cmd tea.Cmd var cmd tea.Cmd
switch msg := msg.(type) { switch msg := msg.(type) {
case tea.KeyMsg: case tea.KeyMsg:
if m.ActiveWindow == "calendarChoose" { switch m.ActiveWindow {
case "today": //TODO rm me debug
switch keypress := msg.String(); keypress {
case "q", "ctrl+c":
// m.quitting = true
return m, tea.Quit
case "y":
// m."2797322749061742597"
case "h":
todoInfo := TodoInterface{
name: "testName1",
description: "description",
priority: 3,
dueTime: time.Now(),
alarmOffset: "1h",
}
task, err := CreateTodo(todoInfo)
if err != nil {return m, m.errHandler(err,"test fail")}
err = m.UploadTodo(task)
if err != nil {return m, m.errHandler(err,"test fail2")}
m.UpdateTodos(task)
// fmt.Println(&task)
// fmt.Println(err)
// time.Sleep(2*time.Second)
return m, nil
}
case "calendarChoose":
switch keypress := msg.String(); keypress { switch keypress := msg.String(); keypress {
case "q", "ctrl+c": case "q", "ctrl+c":
// m.quitting = true // m.quitting = true
@ -91,49 +116,49 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
err := m.CredentialsSave() err := m.CredentialsSave()
if err != nil { return m, m.errHandler("Failed to save credentials",err)} if err != nil { return m, m.errHandler(err,"Failed to save credentials")}
m.CalendarToTodo() m.CalendarToTodo()
return m, nil return m, nil
} }
}
if m.ActiveWindow == "login" {
switch keypress := msg.String(); keypress { case "login":
case "ctrl+c", "q": switch keypress := msg.String(); keypress {
return m, tea.Quit case "ctrl+c", "q":
case "enter": return m, tea.Quit
if m.focused == len(m.loginInputs)-1 { case "enter":
//TODO check that we have all fields not empty and notificate about it if m.focused == len(m.loginInputs)-1 {
//TODO submit //TODO check that we have all fields not empty and notificate about it
for i := range m.loginInputs { //TODO submit
m.loginInputs[i], cmd = m.loginInputs[i].Update(msg) for i := range m.loginInputs {
} m.loginInputs[i], cmd = m.loginInputs[i].Update(msg)
// fmt.Println(m.loginInputs[url].Value())//DEBUG }
m.Creds.URL = m.loginInputs[url].Value() // fmt.Println(m.loginInputs[url].Value())//DEBUG
m.Creds.Username = m.loginInputs[login].Value() m.Creds.URL = m.loginInputs[url].Value()
m.Creds.Password = m.loginInputs[pass].Value() m.Creds.Username = m.loginInputs[login].Value()
// fmt.Println(m.Creds.URL)//DEBUG m.Creds.Password = m.loginInputs[pass].Value()
// fmt.Println(m.Creds.URL)//DEBUG
// time.Sleep(1 * time.Second) ///DEBUG
// m.Creds. // time.Sleep(1 * time.Second) ///DEBUG
// return m, nil // m.Creds.
err := m.LoginToCalendar() // return m, nil
if err != nil {return m, m.errHandler("Failed to authenticate",err)} err := m.LoginToCalendar()
// try login -> choose calendar -> store -> move to getting stuff if err != nil {return m, m.errHandler(err,"Failed to authenticate")}
return m, nil // try login -> choose calendar -> store -> move to getting stuff
// return m, tea.Quit return m, nil
// return m, tea.Quit
}
m.nextInput()
case "shift+tab", "up":
m.prevInput()
case "tab", "down":
m.nextInput()
} }
m.nextInput() for i := range m.loginInputs {
case "shift+tab", "up": m.loginInputs[i].Blur()
m.prevInput() }
case "tab", "down": m.loginInputs[m.focused].Focus()
m.nextInput()
}
for i := range m.loginInputs {
m.loginInputs[i].Blur()
}
m.loginInputs[m.focused].Focus()
} }
switch keypress := msg.String(); keypress { switch keypress := msg.String(); keypress {