Repository: olup/kobowriter Branch: main Commit: 80564fb6ac46 Files: 18 Total size: 33.4 KB Directory structure: gitextract__t0h6qw4/ ├── .gitignore ├── Makefile ├── README.md ├── event/ │ ├── key.go │ └── keyEvents.go ├── findKeyboard.go ├── go.mod ├── go.sum ├── main.go ├── matrix/ │ └── matrix.go ├── screener/ │ ├── image.go │ └── screen.go ├── utils/ │ ├── text.go │ └── utils.go └── views/ ├── document.go ├── menu.go ├── qr.go └── textView.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ kobowriter ================================================ FILE: Makefile ================================================ .PHONY: build build: CGO_ENABLED=1 GOARCH=arm GOOS=linux CC=${CROSS_TC}-gcc CXX=${CROSS_TC}-g++ go build -o ./build/kobowriter ================================================ FILE: README.md ================================================

# Kobowriter This small project aims to let you use your old KOBO e-reader (mine is a GLO HD) as a simple, distraction free typewriter. For years I thought that e-ink was the ultimate medium to write in broad daylight without eye strain or focus fatigue. It seems that others have had the same ideas, as we can see in the [Freewrite](https://getfreewrite.com/) or [Pomera](https://www.kickstarter.com/projects/2132003782/pomera-pocket-typewriter-with-e-ink?ref=category_newest&ref=discovery) products. This project brings the same form factor in a considerably cheaper way (especially if like me you already have a KOBO at hand). > Note that the installed software should let you use switch between your normal kobo stock software and the KoboWriter one; so your kobo is still usable in its default way. > Because XCSoar USB OTG should work for many KOBO devices (touch, Mini, Glo HD and pretty much all the later ones), this project would work there too. But as of now this program has only been built and tested for the KOBO GLO HD and only supports the AZERTY (French) keyboard. You can open issues if you need to support other devices / keyboards ## How it looks ![From face](assets/face.jpg) ![From side](assets/side.jpg) *TODO add video* ## How it works The kobo e-readers have a Micro-USB connector to charge and transfer files. With proper kernel modification this USB socket can be used as OTG, letting one plug in any kind of USB device. Such kernel was compiled by the [XCSoar](https://github.com/XCSoar/XCSoar) project in order to turn the kobo into a fliying assistant supported by an external GPS. We use their modifications to connect a USB keyboard to the OTG port. However, the kobo giving no power through its USB socket, the keyboard has to be powered on its own - you can either use a cheap USB otg power cable [like this one](https://www.amazon.com/AuviPal-Micro-USB-Cable-Power/dp/B07FY9Z9GD/ref=sr_1_3?crid=13TQ5BP3TUJT5&dchild=1&keywords=powered+usb+otg&qid=1630094365&sprefix=powered+%2Caps%2C536&sr=8-3) or modify the keyboard, like I did. The software lets you use the keyboard to write and edit text files. It's coded in Go, compiled with a toolchain prepared for the KOBO devices, and relies largely on the excellent [FBInk](https://github.com/NiLuJe/FBInk) library to drive the screen, through its extremely useful port in Go, [go-fbink](https://github.com/shermp/go-fbink-v2). ## How to build it > Note that we also provide ready made precompiled binaries for your KOBO First you need to download and build the **koxtoolchain** on your development computer. This toolchain, once built, will let you build Go programs that can run on the KOBO. *TODO : Detailed step to build project* ## How to install You can build the software, put it on a KOBO with XCSoar software, and launch it any way you see fit. Or you can use our modified XCSoar installer that will get you the XCSoar program, kernel, and Kobowriter in just one step: > You do this at your own risk! - Download the `KoboRoot.tar.gz` from the release page - Connect your Kobo and place the archive in the .kobo (hidden) directory - eject safely, unplug, and let the Kobo update From now on your Kobo will start up on XCSoar launcher. From there you can start the stock Kobo software, turn on USB-OTG or start the KoboWriter. > Note that when USB-OTG is enable, you won't be able to start the stock Kobo software. But you need to have it on in order to use the KoboWriter software. Changing the USB-OTG setting requires a restart. - When you start the KOBO, if not activated yet, from the XCSoar laucher tap on `system` and the `enable USB-OTG` and then restart the device. If, like me, you use the KOBO only for KoboWriter, then your device should always boot in this state. In this case, only this last step is required: - From XCSoar launcher tap `tools` and then `KoboWriter`. Plugin you powered USB keyboard and you should be good to go ;-) ================================================ FILE: event/key.go ================================================ //AZERTYkeybindings package event var KeyCode = map[int]string{ 0: "KEY_RESERVED", 1: "KEY_ESC", 2: "&", 3: "é", 4: "\"", 5: "'", 6: "(", 7: "-", 8: "è", 9: "_", 10: "ç", 11: "à", 12: ")", 13: "=", 14: "KEY_BACKSPACE", 15: "KEY_TAB", 16: "a", 17: "z", 18: "e", 19: "r", 20: "t", 21: "y", 22: "u", 23: "i", 24: "o", 25: "p", 26: "^", 27: "$", 28: "KEY_ENTER", 29: "KEY_L_CTRL", 30: "q", 31: "s", 32: "d", 33: "f", 34: "g", 35: "h", 36: "j", 37: "k", 38: "l", 39: "m", 40: "ù", 41: "*", 42: "KEY_L_SHIFT", 43: "<", 44: "w", 45: "x", 46: "c", 47: "v", 48: "b", 49: "n", 50: ",", 51: ";", 52: ":", 53: "!", 54: "KEY_R_SHIFT", 55: "KEY_KPASTERISK", 56: "KEY_L_ALT", 57: "KEY_SPACE", 58: "KEY_CAPSLOCK", 59: "KEY_F1", 60: "KEY_F2", 61: "KEY_F3", 62: "KEY_F4", 63: "KEY_F5", 64: "KEY_F6", 65: "KEY_F7", 66: "KEY_F8", 67: "KEY_F9", 68: "KEY_F10", 87: "KEY_F11", 88: "KEY_F12", 100: "KEY_ALT_GR", 103: "KEY_UP", 105: "KEY_LEFT", 106: "KEY_RIGHT", 108: "KEY_DOWN", 111: "KEY_DEL", 183: "KEY_F13", 184: "KEY_F14", 185: "KEY_F15", 186: "KEY_F16", 187: "KEY_F17", 188: "KEY_F18", 189: "KEY_F19", 190: "KEY_F20", 191: "KEY_F21", 192: "KEY_F22", 193: "KEY_F23", 194: "KEY_F24", } var KeyCodeMaj = map[int]string{ 2: "1", 3: "2", 4: "3", 5: "4", 6: "5", 7: "6", 8: "7", 9: "8", 10: "9", 11: "0", 12: "°", 13: "+", 16: "A", 17: "Z", 18: "E", 19: "R", 20: "T", 21: "Y", 22: "U", 23: "I", 24: "O", 25: "P", 26: "¨", 27: "£", 30: "Q", 31: "S", 32: "D", 33: "F", 34: "G", 35: "H", 36: "J", 37: "K", 38: "L", 39: "M", 40: "%", 41: "µ", 43: ">", 44: "W", 45: "X", 46: "C", 47: "V", 48: "B", 49: "N", 50: "?", 51: ".", 52: "/", 53: "§", } var KeyCodeAltGr = map[int]string{ 3: "~", 4: "#", 5: "{", 6: "[", 7: "|", 8: "`", 9: "\\", 10: "^", 11: "@", 12: "]", 13: "}", 16: "æ", 17: "«", 18: "€", 19: "¶", 20: "ŧ", 21: "←", 22: "↓", 23: "→", 24: "ø", 25: "þ", 26: "¨", 27: "¤", 30: "@", 31: "ß", 32: "ð", 33: "đ", 34: "ŋ", 35: "ħ", 37: "ĸ", 38: "ł", 39: "µ", 41: "`", 43: "|", 44: "ł", 45: "»", 46: "¢", 47: "“", 48: "”", 49: "n", 50: "´", 51: "─", 52: "·", } ================================================ FILE: event/keyEvents.go ================================================ package event import ( "github.com/MarinX/keylogger" "github.com/asaskevich/EventBus" "github.com/olup/kobowriter/utils" ) type KeyEvent struct { IsCtrl bool IsAlt bool IsAltGr bool IsShift bool IsShiftLock bool KeyCode int IsChar bool KeyChar string KeyValue string } func BindKeyEvent(k *keylogger.KeyLogger, b EventBus.Bus) { event := KeyEvent{ IsShift: false, IsShiftLock: false, IsAltGr: false, IsAlt: false, IsCtrl: false, } events := k.Read() for e := range events { if e.Type == keylogger.EvKey { keyValue := KeyCode[int(e.Code)] if keyValue == "" { continue } event.KeyChar = "" event.IsChar = false event.KeyCode = int(e.Code) event.KeyValue = keyValue if e.KeyPress() { switch keyValue { case "KEY_L_SHIFT", "KEY_R_SHIFT": event.IsShift = true case "KEY_CAPSLOCK": event.IsShiftLock = !event.IsShiftLock case "KEY_ALT_GR": event.IsAltGr = true case "KEY_L_ALT": event.IsAlt = true case "KEY_L_CTRL", "KEY_R_CTRL": event.IsCtrl = true } } if e.KeyRelease() { switch keyValue { case "KEY_L_SHIFT", "KEY_R_SHIFT": event.IsShift = false case "KEY_ALT_GR": event.IsAltGr = false case "KEY_L_GR": event.IsAlt = false case "KEY_L_CTRL", "KEY_R_CTRL": event.IsCtrl = false } } else { // letters if utils.IsLetter(keyValue) { event.IsChar = true if event.IsShift || event.IsShiftLock { event.KeyChar = KeyCodeMaj[int(e.Code)] } else if event.IsAltGr { event.KeyChar = KeyCodeAltGr[int(e.Code)] } else { event.KeyChar = KeyCode[int(e.Code)] } } b.Publish("KEY", event) } } } println("lost keyboadr") b.Publish("REQUIRE_KEYBOARD") } ================================================ FILE: findKeyboard.go ================================================ package main import ( "fmt" "os/exec" "time" "github.com/MarinX/keylogger" "github.com/asaskevich/EventBus" "github.com/olup/kobowriter/event" "github.com/olup/kobowriter/screener" ) func findKeyboard(screen *screener.Screen, bus EventBus.Bus) { // get key logger keyboard := keylogger.FindKeyboardDevice() buttonLogger, _ := keylogger.New("/dev/input/event0") buttonChannel := buttonLogger.Read() screen.Clear() for len(keyboard) <= 0 { screen.PrintAlert("No keyboard found.\n\nPlug your keyboard or clic main button to quit.\n\nNote that [USB OTG MODE] must be turned on in order to detect the keyboard.", 30) time.Sleep(1 * time.Second) select { case _ = <-buttonChannel: println("Quitting program") bus.Publish("QUIT") exec.Command("/opt/xcsoar/bin/KoboMenu").Start() return default: } keyboard = keylogger.FindKeyboardDevice() } screen.Clear() fmt.Println("Found a keyboard at", keyboard) k, _ := keylogger.New(keyboard) go event.BindKeyEvent(k, bus) bus.Publish("ROUTING", "document") return } ================================================ FILE: go.mod ================================================ module github.com/olup/kobowriter go 1.16 require ( github.com/MarinX/keylogger v0.0.0-20210528193429-a54d7834cc1a github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef github.com/fogleman/gg v1.3.0 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/matoous/go-nanoid/v2 v2.0.0 github.com/shermp/go-fbink-v2 v1.20.2 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect ) ================================================ FILE: go.sum ================================================ github.com/MarinX/keylogger v0.0.0-20210528193429-a54d7834cc1a h1:ItKXWegGGThcahUf+ylKFa5pwqkRJofaOyeGdzwO2mM= github.com/MarinX/keylogger v0.0.0-20210528193429-a54d7834cc1a/go.mod h1:aKzZ7D15UvH5LboXkeLmcNi+s/f805vUfB+BfW1fqd4= github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM= github.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek= github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= 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/shermp/go-fbink-v2 v1.20.2 h1:EtRKDZwrc8fkNGDZppsYB2nxcMosYU7hqYLovMP78/4= github.com/shermp/go-fbink-v2 v1.20.2/go.mod h1:88bOAwruwze/4JB/KW8uoyPtWm5OPa1BZEraFMHJgpQ= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 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= ================================================ FILE: main.go ================================================ package main import ( "fmt" "os/exec" "github.com/asaskevich/EventBus" _ "embed" "github.com/olup/kobowriter/screener" "github.com/olup/kobowriter/utils" "github.com/olup/kobowriter/views" ) var saveLocation = "/mnt/onboard/.adds/kobowriter" var filename = "autosave.txt" func main() { fmt.Println("Program started") // kill all nickel related stuff. Will need a reboot to find back the usual fmt.Println("Killing XCSoar programs ...") exec.Command("killall", "-s", "SIGKILL", "KoboMenu").Run() // rotate screen fmt.Println("Rotate screen ...") exec.Command(`fbdepth`, `--rota`, `2`).Run() // initialise fbink fmt.Println("Init FBInk ...") screen := screener.InitScreen() defer screen.Clean() bus := EventBus.New() c := make(chan bool) defer close(c) bus.SubscribeAsync("REQUIRE_KEYBOARD", func() { findKeyboard(screen, bus) }, false) bus.SubscribeAsync("QUIT", func() { screen.PrintAlert("Good Bye !", 500) // quitting c <- true return }, false) var unmount func() bus.SubscribeAsync("ROUTING", func(routeName string) { if unmount != nil { unmount() } switch routeName { case "document": config := utils.LoadConfig(saveLocation) unmount = views.Document(screen, bus, config.LastOpenedDocument) case "menu": unmount = views.MainMenu(screen, bus, saveLocation) case "file-menu": unmount = views.FileMenu(screen, bus, saveLocation) case "settings-menu": unmount = views.SettingsMenu(screen, bus, saveLocation) case "qr": unmount = views.Qr(screen, bus, saveLocation) default: unmount = views.Document(screen, bus, "") } }, false) // init bus.Publish("REQUIRE_KEYBOARD") for quit := range c { if quit { break } } println("yo") } ================================================ FILE: matrix/matrix.go ================================================ package matrix import ( "strings" "github.com/olup/kobowriter/utils" ) type MatrixElement struct { Content rune IsInverted bool } type Matrix [][]MatrixElement func CreateNewMatrix(width int, height int) Matrix { a := make([][]MatrixElement, height) for i := range a { a[i] = make([]MatrixElement, width) for j := range a[i] { a[i][j] = MatrixElement{ Content: ' ', IsInverted: false, } } } return a } func CreateMatrixFromText(text string, width int) Matrix { wrapped := utils.WrapText(text, int(width)) wrapedArray := strings.Split(wrapped, "\n") result := CreateNewMatrix(width, len(wrapedArray)) for i := range result { for j := range result[i] { if j < utils.LenString(wrapedArray[i]) { result[i][j].Content = []rune(wrapedArray[i])[j] } } } return result } func PasteMatrix(baseMatrix Matrix, topMatrix Matrix, offsetX int, offsetY int) Matrix { resultMatrix := CopyMatrix(baseMatrix) for i := range resultMatrix { localI := i - offsetY if localI < 0 || localI >= len(topMatrix) { continue } for j := range resultMatrix[i] { localJ := j - offsetX if localJ < 0 || localJ >= len(topMatrix[localI]) { continue } resultMatrix[i][j] = topMatrix[localI][localJ] } } return resultMatrix } func MatrixToText(matrix Matrix) string { stringz := make([]string, len(matrix)) for i := range matrix { for _, elem := range matrix[i] { stringz[i] = stringz[i] + string(elem.Content) } } return strings.Join(stringz, "") } func InverseMatrix(in Matrix) (out Matrix) { out = in for i := range out { for j, elem := range out[i] { out[i][j].IsInverted = !elem.IsInverted } } return } func FillMatrix(in Matrix, char rune) (out Matrix) { out = CopyMatrix(in) for i := range out { for j := range out[i] { out[i][j].Content = char } } return } func CopyMatrix(in Matrix) (out Matrix) { if len(in) == 0 { return Matrix{} } out = CreateNewMatrix(len(in[0]), len(in)) for i := range out { for j := range out[i] { out[i][j].Content = in[i][j].Content out[i][j].IsInverted = in[i][j].IsInverted } } return } ================================================ FILE: screener/image.go ================================================ package screener import ( "image" "image/png" "io" ) // Get the bi-dimensional pixel array func getPixels(file io.Reader) ([]byte, error) { image.RegisterFormat("png", "png", png.Decode, png.DecodeConfig) img, _, err := image.Decode(file) if err != nil { return nil, err } bounds := img.Bounds() width, height := bounds.Max.X, bounds.Max.Y var pixels []byte for y := 0; y < height; y++ { for x := 0; x < width; x++ { r, g, b, a := img.At(x, y).RGBA() pixels = append(pixels, byte(r/257), byte(g/257), byte(b/257), byte(a/257)) } } return pixels, nil } // Get the bi-dimensional pixel array func getPixelsFromImage(img image.Image) ([]byte, error) { bounds := img.Bounds() width, height := bounds.Max.X, bounds.Max.Y var pixels []byte for y := 0; y < height; y++ { for x := 0; x < width; x++ { r, g, b, a := img.At(x, y).RGBA() pixels = append(pixels, byte(r/257), byte(g/257), byte(b/257), byte(a/257)) } } return pixels, nil } ================================================ FILE: screener/screen.go ================================================ package screener import ( "bytes" "image" "math" "github.com/fogleman/gg" "github.com/olup/kobowriter/matrix" "github.com/shermp/go-fbink-v2/gofbink" ) type Screen struct { originalMatrix matrix.Matrix presentMatrix matrix.Matrix fb *gofbink.FBInk state gofbink.FBInkState Width int Height int fontType string ttSize int } var dc = gg.NewContext(25, 40) var charCache = map[string][]byte{} func InitScreen() (s *Screen) { s = &Screen{} s.fontType = "bitmap" s.state = gofbink.FBInkState{} fbinkOpts := gofbink.FBInkConfig{} rOpts := gofbink.RestrictedConfig{ Fontmult: 3, Fontname: gofbink.Ctrld, } s.fb = gofbink.New(&fbinkOpts, &rOpts) s.fb.Open() s.fb.Init(&fbinkOpts) s.fb.AddOTfont("/mnt/onboard/.adds/kobowriter/inc.ttf", gofbink.FntRegular) s.fb.GetState(&fbinkOpts, &s.state) // clear screen on initialisation s.ClearFlash() if s.fontType == "truetype" { dc.LoadFontFace("inc.ttf", 96) s.ttSize = 40 s.Width = int(s.state.ScreenWidth) / ((s.ttSize / 5) * 3) s.Height = int(s.state.ScreenHeight) / s.ttSize } else { s.Width = int(s.state.MaxCols) s.Height = int(s.state.MaxRows) } s.presentMatrix = matrix.CreateNewMatrix(s.Width, s.Height) s.originalMatrix = matrix.CreateNewMatrix(s.Width, s.Height) println("Screen struct inited") return } func (s *Screen) Clean() { s.fb.Close() } func (s *Screen) Print(matrix matrix.Matrix) { printDiff(s.presentMatrix, matrix, s.fb, s.fontType, s.ttSize) s.presentMatrix = matrix } func same(a matrix.MatrixElement, b matrix.MatrixElement) bool { return a.Content == b.Content && a.IsInverted == b.IsInverted } func printDiff(previous matrix.Matrix, next matrix.Matrix, fb *gofbink.FBInk, fontType string, ttSize int) { for i := range previous { for j := range previous[i] { if !same(previous[i][j], next[i][j]) { if fontType == "truetype" { ttWidth := ((ttSize / 5) * 3) fb.ClearScreen(&gofbink.FBInkConfig{ IsInverted: next[i][j].IsInverted, NoRefresh: true, }, &gofbink.FBInkRect{ Top: uint16(i * ttSize), Left: uint16(j * ttWidth), Height: uint16(ttSize), Width: uint16(ttWidth), }) fb.PrintOT(string(next[i][j].Content), &gofbink.FBInkOTConfig{ Margins: struct { Top int16 Bottom int16 Left int16 Right int16 }{ Top: int16(i * ttSize), Left: int16(j * ttWidth), }, SizePx: uint16(ttSize), IsFormatted: false, }, &gofbink.FBInkConfig{IsInverted: next[i][j].IsInverted, NoRefresh: true}) } else { fb.FBprint(string(next[i][j].Content), &gofbink.FBInkConfig{ Row: int16(i), Col: int16(j), NoRefresh: true, IsInverted: next[i][j].IsInverted, }) } } } } fb.Refresh(0, 0, 0, 0, &gofbink.FBInkConfig{}) } func (s *Screen) PrintPng(imgBytes []byte, w int, h int, x int, y int) { img, _, _ := image.Decode(bytes.NewReader(imgBytes)) buffer, _ := getPixelsFromImage(img) s.fb.PrintRawData(buffer, w, h, uint16(x), uint16(y), &gofbink.FBInkConfig{}) } func getCharImage(s string) []byte { if char, ok := charCache[s]; ok { return char } else { dc.SetRGB(1, 1, 1) dc.Clear() dc.SetRGB(0, 0, 0) dc.DrawString(s, 0, 35) img := dc.Image() buffer, _ := getPixelsFromImage(img) charCache[s] = buffer return buffer } } func (s *Screen) PrintAlert(message string, width int) { thisMatrix := matrix.CreateMatrixFromText(message, width) x := math.Floor((float64(s.state.MaxCols)/2)-float64(width)/2) - 1 y := math.Floor((float64(s.state.MaxRows)/2)-float64(len(thisMatrix))/2) - 1 outerMatrix := matrix.CreateNewMatrix(width+2, len(thisMatrix)+2) thisMatrix = matrix.PasteMatrix(outerMatrix, thisMatrix, 1, 1) thisMatrix = matrix.InverseMatrix(thisMatrix) s.Print(matrix.PasteMatrix(s.originalMatrix, thisMatrix, int(x), int(y))) } func (s *Screen) Clear() { s.fb.ClearScreen(&gofbink.FBInkConfig{}, &gofbink.FBInkRect{}) s.presentMatrix = matrix.FillMatrix(s.presentMatrix, ' ') } func (s *Screen) ClearFlash() { s.fb.ClearScreen(&gofbink.FBInkConfig{IsFlashing: true}, &gofbink.FBInkRect{}) s.presentMatrix = matrix.FillMatrix(s.presentMatrix, ' ') } func (s *Screen) RefreshFlash() { presenMatrix := s.presentMatrix s.ClearFlash() s.Print(presenMatrix) } func (s *Screen) GetOriginalMatrix() matrix.Matrix { return matrix.CopyMatrix(s.originalMatrix) } ================================================ FILE: utils/text.go ================================================ package utils import "strings" func WrapLine(text string, lineWidth int) (wrapped string) { if text == "" { return "" } words := strings.Split(text, " ") if len(words) == 0 { return } wrapped = words[0] spaceLeft := lineWidth - len(wrapped) for _, word := range words[1:] { if LenString(word)+1 > spaceLeft { wrapped += "\n" + word spaceLeft = lineWidth - LenString(word) } else { wrapped += " " + word spaceLeft -= 1 + LenString(word) } } return } func WrapText(text string, lineWidth int) string { lines := strings.Split(text, "\n") if len(lines) == 0 { return "" } for i := range lines { lines[i] = WrapLine(lines[i], lineWidth) } return strings.Join(lines, "\n") } ================================================ FILE: utils/utils.go ================================================ package utils import ( "encoding/json" "os" "path" "strings" "unicode/utf8" gonanoid "github.com/matoous/go-nanoid/v2" ) type Config struct { LastOpenedDocument string `json:"lastOpenDocument"` } func LoadConfig(saveLocation string) Config { content, err := os.ReadFile(path.Join(saveLocation, "config.json")) if err != nil { id, _ := gonanoid.New() return Config{ LastOpenedDocument: id + ".txt", } } var config Config // we unmarshal our byteArray which contains our // jsonFile's content into 'users' which we defined above json.Unmarshal(content, &config) return config } func SaveConfig(config Config, saveLocation string) { // we unmarshal our byteArray which contains our // jsonFile's content into 'users' which we defined above content, _ := json.Marshal(config) os.WriteFile(path.Join(saveLocation, "config.json"), []byte(content), 777) } func IsLetter(s string) bool { return !strings.Contains(s, "KEY") } func InsertAt(text string, insert string, index int) string { if index == LenString(text) { return text + insert } runeText := []rune(text) return string(append(runeText[:index], append([]rune(insert), runeText[index:]...)...)) } func DeleteAt(text string, index int) string { runeText := []rune(text) return string(append(runeText[:index-1], runeText[index:]...)) } func LenString(s string) int { return utf8.RuneCountInString(s) } ================================================ FILE: views/document.go ================================================ package views import ( "os" "path" "time" "github.com/asaskevich/EventBus" "github.com/olup/kobowriter/event" "github.com/olup/kobowriter/matrix" "github.com/olup/kobowriter/screener" "github.com/olup/kobowriter/utils" ) func Document(screen *screener.Screen, bus EventBus.Bus, documentPath string) func() { docContent := []byte("") if documentPath != "" { docContent, _ = os.ReadFile(documentPath) } text := &TextView{ width: int(screen.Width) - 4, height: int(screen.Height) - 2, content: "", scroll: 0, cursorIndex: 0, } text.setContent(string(docContent)) text.setCursorIndex(utils.LenString(string(docContent))) onEvent := func(e event.KeyEvent) { linesToMove := 1 if e.IsCtrl { linesToMove = text.height } // if date combo if e.IsChar { text.setContent(utils.InsertAt(text.content, e.KeyChar, text.cursorIndex)) text.setCursorIndex(text.cursorIndex + 1) } else { // if is modifier key switch e.KeyValue { case "KEY_BACKSPACE": text.setContent(utils.DeleteAt(text.content, text.cursorIndex)) text.setCursorIndex(text.cursorIndex - 1) case "KEY_DEL": if text.cursorIndex < utils.LenString(text.content) { text.setContent(utils.DeleteAt(text.content, text.cursorIndex+1)) } case "KEY_SPACE": text.setContent(utils.InsertAt(text.content, " ", text.cursorIndex)) text.setCursorIndex(text.cursorIndex + 1) case "KEY_ENTER": text.setContent(utils.InsertAt(text.content, "\n", text.cursorIndex)) text.setCursorIndex(text.cursorIndex + 1) case "KEY_RIGHT": text.setCursorIndex(text.cursorIndex + 1) case "KEY_LEFT": text.setCursorIndex(text.cursorIndex - 1) case "KEY_DOWN": text.setCursorPos(Position{ x: text.cursorPos.x, y: text.cursorPos.y + linesToMove, }) case "KEY_UP": text.setCursorPos(Position{ x: text.cursorPos.x, y: text.cursorPos.y - linesToMove, }) case "KEY_ESC": bus.Publish("ROUTING", "menu") case "KEY_F1": text.setContent(utils.InsertAt(text.content, time.Now().Format("02/01/2006"), text.cursorIndex)) text.setCursorIndex(text.cursorIndex + 10) case "KEY_F12": screen.RefreshFlash() } } compiledMatrix := matrix.PasteMatrix(screen.GetOriginalMatrix(), text.renderMatrix(), 2, 1) screen.Print(compiledMatrix) if documentPath != "" { os.WriteFile(path.Join(documentPath), []byte(text.content), 0644) } } bus.SubscribeAsync("KEY", onEvent, false) // display bus.Publish("KEY", event.KeyEvent{}) return func() { bus.Unsubscribe("KEY", onEvent) } } ================================================ FILE: views/menu.go ================================================ package views import ( "os" "os/exec" "path" "sort" "strings" "github.com/asaskevich/EventBus" gonanoid "github.com/matoous/go-nanoid/v2" "github.com/olup/kobowriter/event" "github.com/olup/kobowriter/matrix" "github.com/olup/kobowriter/screener" "github.com/olup/kobowriter/utils" ) type Option struct { label string action func() } func createMenu(title string, options []Option) func(screen *screener.Screen, bus EventBus.Bus) func() { return func(screen *screener.Screen, bus EventBus.Bus) func() { selected := 0 onKey := func(e event.KeyEvent) { if e.KeyValue == "KEY_UP" && selected > 0 { selected-- } if e.KeyValue == "KEY_DOWN" && selected < len(options)-1 { selected++ } if e.KeyValue == "KEY_ENTER" { options[selected].action() } line := 1 matrixx := screen.GetOriginalMatrix() matrixx = matrix.PasteMatrix(matrixx, matrix.CreateMatrixFromText(title+"\n"+strings.Repeat("=", utils.LenString(title)), utils.LenString(title)), 4, line) line += 2 for i, option := range options { optionMatrix := matrix.CreateMatrixFromText(option.label, utils.LenString(option.label)) if selected == i { optionMatrix = matrix.InverseMatrix(optionMatrix) } matrixx = matrix.PasteMatrix(matrixx, optionMatrix, 4, line+i) } screen.Print(matrixx) } bus.SubscribeAsync("KEY", onKey, false) // display bus.Publish("KEY", event.KeyEvent{}) return func() { bus.Unsubscribe("KEY", onKey) } } } func MainMenu(screen *screener.Screen, bus EventBus.Bus, saveLocation string) func() { options := []Option{ { label: "Back", action: func() { bus.Publish("ROUTING", "document") }, }, { label: "Export as QR code", action: func() { bus.Publish("ROUTING", "qr") }, }, { label: "Open Document", action: func() { bus.Publish("ROUTING", "file-menu") }, }, { label: "New Document", action: func() { id, _ := gonanoid.New() config := utils.LoadConfig(saveLocation) config.LastOpenedDocument = path.Join(saveLocation, id+".txt") utils.SaveConfig(config, saveLocation) bus.Publish("ROUTING", "document") }, }, { label: "Settings", action: func() { bus.Publish("ROUTING", "settings-menu") }, }, { label: "Quit to XCSoar", action: func() { exec.Command("/opt/xcsoar/bin/KoboMenu").Start() bus.Publish("QUIT") }, }, } return createMenu("Menu", options)(screen, bus) } func FileMenu(screen *screener.Screen, bus EventBus.Bus, saveLocation string) func() { files, _ := os.ReadDir(saveLocation) options := []Option{ { label: "Back", action: func() { bus.Publish("ROUTING", "menu") }, }, } sort.Slice(files, func(i, j int) bool { infoI, _ := files[i].Info() modTimeI := infoI.ModTime().Unix() infoJ, _ := files[j].Info() modTimeJ := infoJ.ModTime().Unix() return modTimeI > modTimeJ }) for _, file := range files { if strings.HasSuffix(file.Name(), ".txt") { filePath := path.Join(saveLocation, file.Name()) content, _ := os.ReadFile(path.Join(saveLocation, file.Name())) label := strings.Split(string(content), "\n")[0] if utils.LenString(label) > 30 { label = string([]rune(label)[0:30]) + "..." } options = append(options, Option{ label: label, action: func() { config := utils.LoadConfig(saveLocation) config.LastOpenedDocument = filePath utils.SaveConfig(config, saveLocation) bus.Publish("ROUTING", "document") }, }) } } return createMenu("Open File", options)(screen, bus) } func SettingsMenu(screen *screener.Screen, bus EventBus.Bus, saveLocation string) func() { options := []Option{ { label: "Back", action: func() { bus.Publish("ROUTING", "menu") }, }, { label: "Toggle light", action: func() { lightPath := "/sys/class/backlight/mxc_msp430_fl.0/brightness" light := "0" presentLightRaw, _ := os.ReadFile(lightPath) presentLight := strings.TrimSuffix(string(presentLightRaw), "\n") if presentLight == "0" { light = "10" } else { light = "0" } os.WriteFile(lightPath, []byte(light), os.ModePerm) }, }, } return createMenu("Open File", options)(screen, bus) } ================================================ FILE: views/qr.go ================================================ package views import ( "os" "github.com/asaskevich/EventBus" "github.com/olup/kobowriter/event" "github.com/olup/kobowriter/screener" "github.com/olup/kobowriter/utils" "github.com/skip2/go-qrcode" ) func Qr(screen *screener.Screen, bus EventBus.Bus, saveLocation string) func() { onKey := func(event event.KeyEvent) { screen.Clear() bus.Publish("ROUTING", "menu") } bus.SubscribeAsync("KEY", onKey, false) // Display QR on mount screen.Clear() config := utils.LoadConfig(saveLocation) content, err := os.ReadFile(config.LastOpenedDocument) if err != nil { bus.Publish("ROUTING", "menu") } image, _ := qrcode.Encode(string(content), qrcode.High, 800) screen.PrintPng(image, 800, 800, 100, 100) return func() { bus.Unsubscribe("KEY", onKey) } } ================================================ FILE: views/textView.go ================================================ package views import ( "strings" "unicode/utf8" "github.com/olup/kobowriter/matrix" "github.com/olup/kobowriter/utils" ) type TextView struct { content string width int height int wrapContent []string cursorIndex int cursorPos Position lineCount []int scroll int } type Position struct { x int y int } func (t *TextView) init(width int) { t.width = width } func (t *TextView) setContent(text string) { t.content = text t.wrapContent = strings.Split(utils.WrapText(text, t.width), "\n") lineCount := []int{} for _, line := range t.wrapContent { lineCount = append(lineCount, utf8.RuneCountInString(line)+1) } t.lineCount = lineCount } func (t *TextView) setCursorIndex(index int) { // Bounds if index < 0 { index = 0 } if index > utils.LenString(t.content) { index = utils.LenString(t.content) } // Processing t.cursorIndex = index x := 0 y := 0 agg := 0 for i, count := range t.lineCount { aggNext := count + agg if aggNext > t.cursorIndex { y = i x = t.cursorIndex - agg break } agg = aggNext } t.cursorPos = Position{ x, y, } t.updateScroll() } func (t *TextView) setCursorPos(position Position) { // Bounds if position.y < 0 { position.y = 0 } if position.x < 0 { position.x = 0 } if position.y > len(t.lineCount)-1 { position.y = len(t.lineCount) - 1 } if t.lineCount[position.y]-1 < position.x { position.x = t.lineCount[position.y] - 1 } // Procesing agg := 0 for i := 0; i < position.y; i++ { agg += t.lineCount[i] } agg += position.x t.cursorPos = position t.cursorIndex = agg t.updateScroll() } func (t *TextView) renderMatrix() matrix.Matrix { textMatrix := matrix.CreateMatrixFromText(t.content, t.width) if t.cursorPos.x >= 0 && t.cursorPos.y >= 0 && t.cursorPos.x < t.width { textMatrix[t.cursorPos.y][t.cursorPos.x].IsInverted = true } endBound := t.scroll + t.height if endBound > len(textMatrix) { endBound = len(textMatrix) } scrolledTextMatrix := textMatrix[t.scroll:endBound] return scrolledTextMatrix } func (t *TextView) updateScroll() { y := t.cursorPos.y if y > t.scroll+t.height-1 { t.scroll = y - 5 } if y < t.scroll { t.scroll = y - t.height + 5 } if t.scroll > len(t.wrapContent) { t.scroll = len(t.wrapContent) - 5 } if t.scroll < 0 { t.scroll = 0 } }