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


*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
}
}