[
  {
    "path": ".gitignore",
    "content": "kobowriter"
  },
  {
    "path": "Makefile",
    "content": ".PHONY: build\nbuild: \n\tCGO_ENABLED=1 GOARCH=arm GOOS=linux CC=${CROSS_TC}-gcc CXX=${CROSS_TC}-g++ go build -o ./build/kobowriter"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img src=\"assets/kobowriter.png\" />\n</p>\n\n# Kobowriter\n\nThis small project aims to let you use your old KOBO e-reader (mine is a GLO HD) as a simple, distraction free typewriter.\n\nFor 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&amp;ref=discovery) products.\n\nThis project brings the same form factor in a considerably cheaper way (especially if like me you already have a KOBO at hand).\n\n> 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.\n\n> 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\n\n## How it looks\n\n![From face](assets/face.jpg)\n\n![From side](assets/side.jpg)\n\n*TODO add video*\n\n## How it works\n\nThe 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.\n\nSuch 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. \n\nWe use their modifications to connect a USB keyboard to the OTG port.\n\nHowever, 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.\n\nThe 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).\n\n## How to build it\n\n> Note that we also provide ready made precompiled binaries for your KOBO\n\nFirst 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.\n\n*TODO : Detailed step to build project*\n\n## How to install\n\nYou can build the software, put it on a KOBO with XCSoar software, and launch it any way you see fit.\n\nOr you can use our modified XCSoar installer that will get you the XCSoar program, kernel, and Kobowriter in just one step:\n\n> You do this at your own risk!\n\n- Download the `KoboRoot.tar.gz` from the release page\n- Connect your Kobo and place the archive in the .kobo (hidden) directory\n- eject safely, unplug, and let the Kobo update\n  \nFrom 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.\n\n> 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.\n\n- 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.\n\nIf, 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:\n\n- From XCSoar launcher tap `tools` and then `KoboWriter`.\n\nPlugin you powered USB keyboard and you should be good to go ;-)\n"
  },
  {
    "path": "event/key.go",
    "content": "//AZERTYkeybindings\n\npackage event\n\nvar KeyCode = map[int]string{\n\t0: \"KEY_RESERVED\",\n\t1: \"KEY_ESC\",\n\n\t2:  \"&\",\n\t3:  \"é\",\n\t4:  \"\\\"\",\n\t5:  \"'\",\n\t6:  \"(\",\n\t7:  \"-\",\n\t8:  \"è\",\n\t9:  \"_\",\n\t10: \"ç\",\n\t11: \"à\",\n\t12: \")\",\n\t13: \"=\",\n\n\t14: \"KEY_BACKSPACE\",\n\t15: \"KEY_TAB\",\n\n\t16: \"a\",\n\t17: \"z\",\n\t18: \"e\",\n\t19: \"r\",\n\t20: \"t\",\n\t21: \"y\",\n\t22: \"u\",\n\t23: \"i\",\n\t24: \"o\",\n\t25: \"p\",\n\t26: \"^\",\n\t27: \"$\",\n\t28: \"KEY_ENTER\",\n\t29: \"KEY_L_CTRL\",\n\n\t30: \"q\",\n\t31: \"s\",\n\t32: \"d\",\n\t33: \"f\",\n\t34: \"g\",\n\t35: \"h\",\n\t36: \"j\",\n\t37: \"k\",\n\t38: \"l\",\n\t39: \"m\",\n\t40: \"ù\",\n\t41: \"*\",\n\n\t42: \"KEY_L_SHIFT\",\n\t43: \"<\",\n\t44: \"w\",\n\t45: \"x\",\n\t46: \"c\",\n\t47: \"v\",\n\t48: \"b\",\n\t49: \"n\",\n\t50: \",\",\n\t51: \";\",\n\t52: \":\",\n\t53: \"!\",\n\t54: \"KEY_R_SHIFT\",\n\n\t55: \"KEY_KPASTERISK\",\n\t56: \"KEY_L_ALT\",\n\n\t57: \"KEY_SPACE\",\n\t58: \"KEY_CAPSLOCK\",\n\t59: \"KEY_F1\",\n\t60: \"KEY_F2\",\n\t61: \"KEY_F3\",\n\t62: \"KEY_F4\",\n\t63: \"KEY_F5\",\n\t64: \"KEY_F6\",\n\t65: \"KEY_F7\",\n\t66: \"KEY_F8\",\n\t67: \"KEY_F9\",\n\t68: \"KEY_F10\",\n\n\t87: \"KEY_F11\",\n\t88: \"KEY_F12\",\n\n\t100: \"KEY_ALT_GR\",\n\n\t103: \"KEY_UP\",\n\t105: \"KEY_LEFT\",\n\t106: \"KEY_RIGHT\",\n\t108: \"KEY_DOWN\",\n\n\t111: \"KEY_DEL\",\n\n\t183: \"KEY_F13\",\n\t184: \"KEY_F14\",\n\t185: \"KEY_F15\",\n\t186: \"KEY_F16\",\n\t187: \"KEY_F17\",\n\t188: \"KEY_F18\",\n\t189: \"KEY_F19\",\n\t190: \"KEY_F20\",\n\t191: \"KEY_F21\",\n\t192: \"KEY_F22\",\n\t193: \"KEY_F23\",\n\t194: \"KEY_F24\",\n}\n\nvar KeyCodeMaj = map[int]string{\n\t2:  \"1\",\n\t3:  \"2\",\n\t4:  \"3\",\n\t5:  \"4\",\n\t6:  \"5\",\n\t7:  \"6\",\n\t8:  \"7\",\n\t9:  \"8\",\n\t10: \"9\",\n\t11: \"0\",\n\t12: \"°\",\n\t13: \"+\",\n\n\t16: \"A\",\n\t17: \"Z\",\n\t18: \"E\",\n\t19: \"R\",\n\t20: \"T\",\n\t21: \"Y\",\n\t22: \"U\",\n\t23: \"I\",\n\t24: \"O\",\n\t25: \"P\",\n\t26: \"¨\",\n\t27: \"£\",\n\n\t30: \"Q\",\n\t31: \"S\",\n\t32: \"D\",\n\t33: \"F\",\n\t34: \"G\",\n\t35: \"H\",\n\t36: \"J\",\n\t37: \"K\",\n\t38: \"L\",\n\t39: \"M\",\n\t40: \"%\",\n\t41: \"µ\",\n\n\t43: \">\",\n\t44: \"W\",\n\t45: \"X\",\n\t46: \"C\",\n\t47: \"V\",\n\t48: \"B\",\n\t49: \"N\",\n\t50: \"?\",\n\t51: \".\",\n\t52: \"/\",\n\t53: \"§\",\n}\n\nvar KeyCodeAltGr = map[int]string{\n\t3:  \"~\",\n\t4:  \"#\",\n\t5:  \"{\",\n\t6:  \"[\",\n\t7:  \"|\",\n\t8:  \"`\",\n\t9:  \"\\\\\",\n\t10: \"^\",\n\t11: \"@\",\n\t12: \"]\",\n\t13: \"}\",\n\n\t16: \"æ\",\n\t17: \"«\",\n\t18: \"€\",\n\t19: \"¶\",\n\t20: \"ŧ\",\n\t21: \"←\",\n\t22: \"↓\",\n\t23: \"→\",\n\t24: \"ø\",\n\t25: \"þ\",\n\t26: \"¨\",\n\t27: \"¤\",\n\n\t30: \"@\",\n\t31: \"ß\",\n\t32: \"ð\",\n\t33: \"đ\",\n\t34: \"ŋ\",\n\t35: \"ħ\",\n\n\t37: \"ĸ\",\n\t38: \"ł\",\n\t39: \"µ\",\n\n\t41: \"`\",\n\n\t43: \"|\",\n\t44: \"ł\",\n\t45: \"»\",\n\t46: \"¢\",\n\t47: \"“\",\n\t48: \"”\",\n\t49: \"n\",\n\t50: \"´\",\n\t51: \"─\",\n\t52: \"·\",\n}\n"
  },
  {
    "path": "event/keyEvents.go",
    "content": "package event\n\nimport (\n\t\"github.com/MarinX/keylogger\"\n\t\"github.com/asaskevich/EventBus\"\n\t\"github.com/olup/kobowriter/utils\"\n)\n\ntype KeyEvent struct {\n\tIsCtrl      bool\n\tIsAlt       bool\n\tIsAltGr     bool\n\tIsShift     bool\n\tIsShiftLock bool\n\tKeyCode     int\n\tIsChar      bool\n\tKeyChar     string\n\tKeyValue    string\n}\n\nfunc BindKeyEvent(k *keylogger.KeyLogger, b EventBus.Bus) {\n\tevent := KeyEvent{\n\t\tIsShift:     false,\n\t\tIsShiftLock: false,\n\t\tIsAltGr:     false,\n\t\tIsAlt:       false,\n\t\tIsCtrl:      false,\n\t}\n\n\tevents := k.Read()\n\tfor e := range events {\n\t\tif e.Type == keylogger.EvKey {\n\n\t\t\tkeyValue := KeyCode[int(e.Code)]\n\t\t\tif keyValue == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tevent.KeyChar = \"\"\n\t\t\tevent.IsChar = false\n\t\t\tevent.KeyCode = int(e.Code)\n\t\t\tevent.KeyValue = keyValue\n\n\t\t\tif e.KeyPress() {\n\t\t\t\tswitch keyValue {\n\t\t\t\tcase \"KEY_L_SHIFT\", \"KEY_R_SHIFT\":\n\t\t\t\t\tevent.IsShift = true\n\t\t\t\tcase \"KEY_CAPSLOCK\":\n\t\t\t\t\tevent.IsShiftLock = !event.IsShiftLock\n\t\t\t\tcase \"KEY_ALT_GR\":\n\t\t\t\t\tevent.IsAltGr = true\n\t\t\t\tcase \"KEY_L_ALT\":\n\t\t\t\t\tevent.IsAlt = true\n\t\t\t\tcase \"KEY_L_CTRL\", \"KEY_R_CTRL\":\n\t\t\t\t\tevent.IsCtrl = true\n\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif e.KeyRelease() {\n\t\t\t\tswitch keyValue {\n\t\t\t\tcase \"KEY_L_SHIFT\", \"KEY_R_SHIFT\":\n\t\t\t\t\tevent.IsShift = false\n\t\t\t\tcase \"KEY_ALT_GR\":\n\t\t\t\t\tevent.IsAltGr = false\n\t\t\t\tcase \"KEY_L_GR\":\n\t\t\t\t\tevent.IsAlt = false\n\t\t\t\tcase \"KEY_L_CTRL\", \"KEY_R_CTRL\":\n\t\t\t\t\tevent.IsCtrl = false\n\t\t\t\t}\n\t\t\t} else {\n\n\t\t\t\t// letters\n\t\t\t\tif utils.IsLetter(keyValue) {\n\t\t\t\t\tevent.IsChar = true\n\t\t\t\t\tif event.IsShift || event.IsShiftLock {\n\t\t\t\t\t\tevent.KeyChar = KeyCodeMaj[int(e.Code)]\n\t\t\t\t\t} else if event.IsAltGr {\n\t\t\t\t\t\tevent.KeyChar = KeyCodeAltGr[int(e.Code)]\n\t\t\t\t\t} else {\n\t\t\t\t\t\tevent.KeyChar = KeyCode[int(e.Code)]\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tb.Publish(\"KEY\", event)\n\t\t\t}\n\n\t\t}\n\t}\n\tprintln(\"lost keyboadr\")\n\tb.Publish(\"REQUIRE_KEYBOARD\")\n}\n"
  },
  {
    "path": "findKeyboard.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\t\"time\"\n\n\t\"github.com/MarinX/keylogger\"\n\t\"github.com/asaskevich/EventBus\"\n\n\t\"github.com/olup/kobowriter/event\"\n\t\"github.com/olup/kobowriter/screener\"\n)\n\nfunc findKeyboard(screen *screener.Screen, bus EventBus.Bus) {\n\t// get key logger\n\tkeyboard := keylogger.FindKeyboardDevice()\n\n\tbuttonLogger, _ := keylogger.New(\"/dev/input/event0\")\n\tbuttonChannel := buttonLogger.Read()\n\n\tscreen.Clear()\n\n\tfor len(keyboard) <= 0 {\n\t\tscreen.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)\n\t\ttime.Sleep(1 * time.Second)\n\t\tselect {\n\t\tcase _ = <-buttonChannel:\n\t\t\tprintln(\"Quitting program\")\n\t\t\tbus.Publish(\"QUIT\")\n\t\t\texec.Command(\"/opt/xcsoar/bin/KoboMenu\").Start()\n\t\t\treturn\n\t\tdefault:\n\t\t}\n\t\tkeyboard = keylogger.FindKeyboardDevice()\n\t}\n\n\tscreen.Clear()\n\tfmt.Println(\"Found a keyboard at\", keyboard)\n\n\tk, _ := keylogger.New(keyboard)\n\tgo event.BindKeyEvent(k, bus)\n\tbus.Publish(\"ROUTING\", \"document\")\n\treturn\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/olup/kobowriter\n\ngo 1.16\n\nrequire (\n\tgithub.com/MarinX/keylogger v0.0.0-20210528193429-a54d7834cc1a\n\tgithub.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef\n\tgithub.com/fogleman/gg v1.3.0\n\tgithub.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect\n\tgithub.com/matoous/go-nanoid/v2 v2.0.0\n\tgithub.com/shermp/go-fbink-v2 v1.20.2\n\tgithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e\n\tgolang.org/x/image v0.0.0-20210628002857-a66eb6448b8d // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/MarinX/keylogger v0.0.0-20210528193429-a54d7834cc1a h1:ItKXWegGGThcahUf+ylKFa5pwqkRJofaOyeGdzwO2mM=\ngithub.com/MarinX/keylogger v0.0.0-20210528193429-a54d7834cc1a/go.mod h1:aKzZ7D15UvH5LboXkeLmcNi+s/f805vUfB+BfW1fqd4=\ngithub.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef h1:2JGTg6JapxP9/R33ZaagQtAM4EkkSYnIAlOG5EI8gkM=\ngithub.com/asaskevich/EventBus v0.0.0-20200907212545-49d423059eef/go.mod h1:JS7hed4L1fj0hXcyEejnW57/7LCetXggd+vwrRnYeII=\ngithub.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=\ngithub.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=\ngithub.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=\ngithub.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=\ngithub.com/matoous/go-nanoid v1.5.0 h1:VRorl6uCngneC4oUQqOYtO3S0H5QKFtKuKycFG3euek=\ngithub.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=\ngithub.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=\ngithub.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/shermp/go-fbink-v2 v1.20.2 h1:EtRKDZwrc8fkNGDZppsYB2nxcMosYU7hqYLovMP78/4=\ngithub.com/shermp/go-fbink-v2 v1.20.2/go.mod h1:88bOAwruwze/4JB/KW8uoyPtWm5OPa1BZEraFMHJgpQ=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=\ngithub.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngolang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=\ngolang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"fmt\"\n\t\"os/exec\"\n\n\t\"github.com/asaskevich/EventBus\"\n\n\t_ \"embed\"\n\n\t\"github.com/olup/kobowriter/screener\"\n\t\"github.com/olup/kobowriter/utils\"\n\t\"github.com/olup/kobowriter/views\"\n)\n\nvar saveLocation = \"/mnt/onboard/.adds/kobowriter\"\nvar filename = \"autosave.txt\"\n\nfunc main() {\n\tfmt.Println(\"Program started\")\n\n\t// kill all nickel related stuff. Will need a reboot to find back the usual\n\tfmt.Println(\"Killing XCSoar programs ...\")\n\texec.Command(\"killall\", \"-s\", \"SIGKILL\", \"KoboMenu\").Run()\n\n\t// rotate screen\n\tfmt.Println(\"Rotate screen ...\")\n\texec.Command(`fbdepth`, `--rota`, `2`).Run()\n\n\t// initialise fbink\n\tfmt.Println(\"Init FBInk ...\")\n\n\tscreen := screener.InitScreen()\n\tdefer screen.Clean()\n\n\tbus := EventBus.New()\n\n\tc := make(chan bool)\n\tdefer close(c)\n\n\tbus.SubscribeAsync(\"REQUIRE_KEYBOARD\", func() {\n\t\tfindKeyboard(screen, bus)\n\t}, false)\n\n\tbus.SubscribeAsync(\"QUIT\", func() {\n\t\tscreen.PrintAlert(\"Good Bye !\", 500)\n\n\t\t// quitting\n\t\tc <- true\n\t\treturn\n\t}, false)\n\n\tvar unmount func()\n\tbus.SubscribeAsync(\"ROUTING\", func(routeName string) {\n\t\tif unmount != nil {\n\t\t\tunmount()\n\t\t}\n\n\t\tswitch routeName {\n\t\tcase \"document\":\n\t\t\tconfig := utils.LoadConfig(saveLocation)\n\t\t\tunmount = views.Document(screen, bus, config.LastOpenedDocument)\n\t\tcase \"menu\":\n\t\t\tunmount = views.MainMenu(screen, bus, saveLocation)\n\t\tcase \"file-menu\":\n\t\t\tunmount = views.FileMenu(screen, bus, saveLocation)\n\t\tcase \"settings-menu\":\n\t\t\tunmount = views.SettingsMenu(screen, bus, saveLocation)\n\t\tcase \"qr\":\n\t\t\tunmount = views.Qr(screen, bus, saveLocation)\n\n\t\tdefault:\n\t\t\tunmount = views.Document(screen, bus, \"\")\n\t\t}\n\n\t}, false)\n\n\t// init\n\tbus.Publish(\"REQUIRE_KEYBOARD\")\n\n\tfor quit := range c {\n\t\tif quit {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tprintln(\"yo\")\n\n}\n"
  },
  {
    "path": "matrix/matrix.go",
    "content": "package matrix\n\nimport (\n\t\"strings\"\n\n\t\"github.com/olup/kobowriter/utils\"\n)\n\ntype MatrixElement struct {\n\tContent    rune\n\tIsInverted bool\n}\n\ntype Matrix [][]MatrixElement\n\nfunc CreateNewMatrix(width int, height int) Matrix {\n\ta := make([][]MatrixElement, height)\n\tfor i := range a {\n\t\ta[i] = make([]MatrixElement, width)\n\t\tfor j := range a[i] {\n\t\t\ta[i][j] = MatrixElement{\n\t\t\t\tContent:    ' ',\n\t\t\t\tIsInverted: false,\n\t\t\t}\n\t\t}\n\t}\n\treturn a\n}\n\nfunc CreateMatrixFromText(text string, width int) Matrix {\n\n\twrapped := utils.WrapText(text, int(width))\n\n\twrapedArray := strings.Split(wrapped, \"\\n\")\n\tresult := CreateNewMatrix(width, len(wrapedArray))\n\n\tfor i := range result {\n\t\tfor j := range result[i] {\n\t\t\tif j < utils.LenString(wrapedArray[i]) {\n\t\t\t\tresult[i][j].Content = []rune(wrapedArray[i])[j]\n\t\t\t}\n\t\t}\n\t}\n\n\treturn result\n}\n\nfunc PasteMatrix(baseMatrix Matrix, topMatrix Matrix, offsetX int, offsetY int) Matrix {\n\tresultMatrix := CopyMatrix(baseMatrix)\n\tfor i := range resultMatrix {\n\t\tlocalI := i - offsetY\n\t\tif localI < 0 || localI >= len(topMatrix) {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor j := range resultMatrix[i] {\n\t\t\tlocalJ := j - offsetX\n\t\t\tif localJ < 0 || localJ >= len(topMatrix[localI]) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tresultMatrix[i][j] = topMatrix[localI][localJ]\n\n\t\t}\n\t}\n\n\treturn resultMatrix\n}\n\nfunc MatrixToText(matrix Matrix) string {\n\tstringz := make([]string, len(matrix))\n\tfor i := range matrix {\n\t\tfor _, elem := range matrix[i] {\n\t\t\tstringz[i] = stringz[i] + string(elem.Content)\n\t\t}\n\t}\n\treturn strings.Join(stringz, \"\")\n}\n\nfunc InverseMatrix(in Matrix) (out Matrix) {\n\tout = in\n\tfor i := range out {\n\t\tfor j, elem := range out[i] {\n\t\t\tout[i][j].IsInverted = !elem.IsInverted\n\t\t}\n\t}\n\treturn\n}\n\nfunc FillMatrix(in Matrix, char rune) (out Matrix) {\n\tout = CopyMatrix(in)\n\tfor i := range out {\n\t\tfor j := range out[i] {\n\t\t\tout[i][j].Content = char\n\t\t}\n\t}\n\treturn\n}\n\nfunc CopyMatrix(in Matrix) (out Matrix) {\n\tif len(in) == 0 {\n\t\treturn Matrix{}\n\t}\n\tout = CreateNewMatrix(len(in[0]), len(in))\n\tfor i := range out {\n\t\tfor j := range out[i] {\n\t\t\tout[i][j].Content = in[i][j].Content\n\t\t\tout[i][j].IsInverted = in[i][j].IsInverted\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "screener/image.go",
    "content": "package screener\n\nimport (\n\t\"image\"\n\t\"image/png\"\n\t\"io\"\n)\n\n// Get the bi-dimensional pixel array\nfunc getPixels(file io.Reader) ([]byte, error) {\n\timage.RegisterFormat(\"png\", \"png\", png.Decode, png.DecodeConfig)\n\timg, _, err := image.Decode(file)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbounds := img.Bounds()\n\twidth, height := bounds.Max.X, bounds.Max.Y\n\n\tvar pixels []byte\n\n\tfor y := 0; y < height; y++ {\n\t\tfor x := 0; x < width; x++ {\n\t\t\tr, g, b, a := img.At(x, y).RGBA()\n\t\t\tpixels = append(pixels, byte(r/257), byte(g/257), byte(b/257), byte(a/257))\n\t\t}\n\t}\n\n\treturn pixels, nil\n}\n\n// Get the bi-dimensional pixel array\nfunc getPixelsFromImage(img image.Image) ([]byte, error) {\n\n\tbounds := img.Bounds()\n\twidth, height := bounds.Max.X, bounds.Max.Y\n\n\tvar pixels []byte\n\n\tfor y := 0; y < height; y++ {\n\t\tfor x := 0; x < width; x++ {\n\t\t\tr, g, b, a := img.At(x, y).RGBA()\n\t\t\tpixels = append(pixels, byte(r/257), byte(g/257), byte(b/257), byte(a/257))\n\t\t}\n\t}\n\n\treturn pixels, nil\n}\n"
  },
  {
    "path": "screener/screen.go",
    "content": "package screener\n\nimport (\n\t\"bytes\"\n\t\"image\"\n\t\"math\"\n\n\t\"github.com/fogleman/gg\"\n\t\"github.com/olup/kobowriter/matrix\"\n\t\"github.com/shermp/go-fbink-v2/gofbink\"\n)\n\ntype Screen struct {\n\toriginalMatrix matrix.Matrix\n\tpresentMatrix  matrix.Matrix\n\tfb             *gofbink.FBInk\n\tstate          gofbink.FBInkState\n\tWidth          int\n\tHeight         int\n\tfontType       string\n\tttSize         int\n}\n\nvar dc = gg.NewContext(25, 40)\nvar charCache = map[string][]byte{}\n\nfunc InitScreen() (s *Screen) {\n\ts = &Screen{}\n\ts.fontType = \"bitmap\"\n\n\ts.state = gofbink.FBInkState{}\n\n\tfbinkOpts := gofbink.FBInkConfig{}\n\trOpts := gofbink.RestrictedConfig{\n\t\tFontmult: 3,\n\t\tFontname: gofbink.Ctrld,\n\t}\n\ts.fb = gofbink.New(&fbinkOpts, &rOpts)\n\n\ts.fb.Open()\n\ts.fb.Init(&fbinkOpts)\n\ts.fb.AddOTfont(\"/mnt/onboard/.adds/kobowriter/inc.ttf\", gofbink.FntRegular)\n\n\ts.fb.GetState(&fbinkOpts, &s.state)\n\n\t// clear screen on initialisation\n\ts.ClearFlash()\n\n\tif s.fontType == \"truetype\" {\n\t\tdc.LoadFontFace(\"inc.ttf\", 96)\n\t\ts.ttSize = 40\n\t\ts.Width = int(s.state.ScreenWidth) / ((s.ttSize / 5) * 3)\n\t\ts.Height = int(s.state.ScreenHeight) / s.ttSize\n\t} else {\n\t\ts.Width = int(s.state.MaxCols)\n\t\ts.Height = int(s.state.MaxRows)\n\t}\n\n\ts.presentMatrix = matrix.CreateNewMatrix(s.Width, s.Height)\n\ts.originalMatrix = matrix.CreateNewMatrix(s.Width, s.Height)\n\n\tprintln(\"Screen struct inited\")\n\n\treturn\n\n}\n\nfunc (s *Screen) Clean() {\n\ts.fb.Close()\n}\n\nfunc (s *Screen) Print(matrix matrix.Matrix) {\n\tprintDiff(s.presentMatrix, matrix, s.fb, s.fontType, s.ttSize)\n\ts.presentMatrix = matrix\n}\n\nfunc same(a matrix.MatrixElement, b matrix.MatrixElement) bool {\n\treturn a.Content == b.Content && a.IsInverted == b.IsInverted\n}\n\nfunc printDiff(previous matrix.Matrix, next matrix.Matrix, fb *gofbink.FBInk, fontType string, ttSize int) {\n\tfor i := range previous {\n\t\tfor j := range previous[i] {\n\t\t\tif !same(previous[i][j], next[i][j]) {\n\t\t\t\tif fontType == \"truetype\" {\n\t\t\t\t\tttWidth := ((ttSize / 5) * 3)\n\t\t\t\t\tfb.ClearScreen(&gofbink.FBInkConfig{\n\t\t\t\t\t\tIsInverted: next[i][j].IsInverted,\n\t\t\t\t\t\tNoRefresh:  true,\n\t\t\t\t\t}, &gofbink.FBInkRect{\n\t\t\t\t\t\tTop:    uint16(i * ttSize),\n\t\t\t\t\t\tLeft:   uint16(j * ttWidth),\n\t\t\t\t\t\tHeight: uint16(ttSize),\n\t\t\t\t\t\tWidth:  uint16(ttWidth),\n\t\t\t\t\t})\n\n\t\t\t\t\tfb.PrintOT(string(next[i][j].Content), &gofbink.FBInkOTConfig{\n\t\t\t\t\t\tMargins: struct {\n\t\t\t\t\t\t\tTop    int16\n\t\t\t\t\t\t\tBottom int16\n\t\t\t\t\t\t\tLeft   int16\n\t\t\t\t\t\t\tRight  int16\n\t\t\t\t\t\t}{\n\t\t\t\t\t\t\tTop:  int16(i * ttSize),\n\t\t\t\t\t\t\tLeft: int16(j * ttWidth),\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSizePx:      uint16(ttSize),\n\t\t\t\t\t\tIsFormatted: false,\n\t\t\t\t\t}, &gofbink.FBInkConfig{IsInverted: next[i][j].IsInverted, NoRefresh: true})\n\n\t\t\t\t} else {\n\t\t\t\t\tfb.FBprint(string(next[i][j].Content), &gofbink.FBInkConfig{\n\t\t\t\t\t\tRow:        int16(i),\n\t\t\t\t\t\tCol:        int16(j),\n\t\t\t\t\t\tNoRefresh:  true,\n\t\t\t\t\t\tIsInverted: next[i][j].IsInverted,\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t}\n\n\t\t}\n\t}\n\n\tfb.Refresh(0, 0, 0, 0, &gofbink.FBInkConfig{})\n}\n\nfunc (s *Screen) PrintPng(imgBytes []byte, w int, h int, x int, y int) {\n\timg, _, _ := image.Decode(bytes.NewReader(imgBytes))\n\tbuffer, _ := getPixelsFromImage(img)\n\ts.fb.PrintRawData(buffer, w, h, uint16(x), uint16(y), &gofbink.FBInkConfig{})\n}\n\nfunc getCharImage(s string) []byte {\n\tif char, ok := charCache[s]; ok {\n\t\treturn char\n\t} else {\n\t\tdc.SetRGB(1, 1, 1)\n\t\tdc.Clear()\n\n\t\tdc.SetRGB(0, 0, 0)\n\t\tdc.DrawString(s, 0, 35)\n\t\timg := dc.Image()\n\t\tbuffer, _ := getPixelsFromImage(img)\n\t\tcharCache[s] = buffer\n\t\treturn buffer\n\t}\n}\n\nfunc (s *Screen) PrintAlert(message string, width int) {\n\tthisMatrix := matrix.CreateMatrixFromText(message, width)\n\tx := math.Floor((float64(s.state.MaxCols)/2)-float64(width)/2) - 1\n\ty := math.Floor((float64(s.state.MaxRows)/2)-float64(len(thisMatrix))/2) - 1\n\touterMatrix := matrix.CreateNewMatrix(width+2, len(thisMatrix)+2)\n\tthisMatrix = matrix.PasteMatrix(outerMatrix, thisMatrix, 1, 1)\n\tthisMatrix = matrix.InverseMatrix(thisMatrix)\n\ts.Print(matrix.PasteMatrix(s.originalMatrix, thisMatrix, int(x), int(y)))\n}\n\nfunc (s *Screen) Clear() {\n\ts.fb.ClearScreen(&gofbink.FBInkConfig{}, &gofbink.FBInkRect{})\n\ts.presentMatrix = matrix.FillMatrix(s.presentMatrix, ' ')\n}\n\nfunc (s *Screen) ClearFlash() {\n\ts.fb.ClearScreen(&gofbink.FBInkConfig{IsFlashing: true}, &gofbink.FBInkRect{})\n\ts.presentMatrix = matrix.FillMatrix(s.presentMatrix, ' ')\n}\n\nfunc (s *Screen) RefreshFlash() {\n\tpresenMatrix := s.presentMatrix\n\ts.ClearFlash()\n\ts.Print(presenMatrix)\n}\n\nfunc (s *Screen) GetOriginalMatrix() matrix.Matrix {\n\treturn matrix.CopyMatrix(s.originalMatrix)\n}\n"
  },
  {
    "path": "utils/text.go",
    "content": "package utils\n\nimport \"strings\"\n\nfunc WrapLine(text string, lineWidth int) (wrapped string) {\n\tif text == \"\" {\n\t\treturn \"\"\n\t}\n\n\twords := strings.Split(text, \" \")\n\tif len(words) == 0 {\n\t\treturn\n\t}\n\twrapped = words[0]\n\tspaceLeft := lineWidth - len(wrapped)\n\tfor _, word := range words[1:] {\n\t\tif LenString(word)+1 > spaceLeft {\n\t\t\twrapped += \"\\n\" + word\n\t\t\tspaceLeft = lineWidth - LenString(word)\n\t\t} else {\n\t\t\twrapped += \" \" + word\n\t\t\tspaceLeft -= 1 + LenString(word)\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc WrapText(text string, lineWidth int) string {\n\tlines := strings.Split(text, \"\\n\")\n\tif len(lines) == 0 {\n\t\treturn \"\"\n\t}\n\tfor i := range lines {\n\t\tlines[i] = WrapLine(lines[i], lineWidth)\n\t}\n\n\treturn strings.Join(lines, \"\\n\")\n\n}\n"
  },
  {
    "path": "utils/utils.go",
    "content": "package utils\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\tgonanoid \"github.com/matoous/go-nanoid/v2\"\n)\n\ntype Config struct {\n\tLastOpenedDocument string `json:\"lastOpenDocument\"`\n}\n\nfunc LoadConfig(saveLocation string) Config {\n\tcontent, err := os.ReadFile(path.Join(saveLocation, \"config.json\"))\n\n\tif err != nil {\n\t\tid, _ := gonanoid.New()\n\t\treturn Config{\n\t\t\tLastOpenedDocument: id + \".txt\",\n\t\t}\n\t}\n\n\tvar config Config\n\n\t// we unmarshal our byteArray which contains our\n\t// jsonFile's content into 'users' which we defined above\n\tjson.Unmarshal(content, &config)\n\treturn config\n}\n\nfunc SaveConfig(config Config, saveLocation string) {\n\n\t// we unmarshal our byteArray which contains our\n\t// jsonFile's content into 'users' which we defined above\n\tcontent, _ := json.Marshal(config)\n\tos.WriteFile(path.Join(saveLocation, \"config.json\"), []byte(content), 777)\n}\n\nfunc IsLetter(s string) bool {\n\treturn !strings.Contains(s, \"KEY\")\n}\n\nfunc InsertAt(text string, insert string, index int) string {\n\tif index == LenString(text) {\n\t\treturn text + insert\n\t}\n\truneText := []rune(text)\n\treturn string(append(runeText[:index], append([]rune(insert), runeText[index:]...)...))\n}\n\nfunc DeleteAt(text string, index int) string {\n\truneText := []rune(text)\n\treturn string(append(runeText[:index-1], runeText[index:]...))\n}\n\nfunc LenString(s string) int {\n\treturn utf8.RuneCountInString(s)\n}\n"
  },
  {
    "path": "views/document.go",
    "content": "package views\n\nimport (\n\t\"os\"\n\t\"path\"\n\t\"time\"\n\n\t\"github.com/asaskevich/EventBus\"\n\t\"github.com/olup/kobowriter/event\"\n\t\"github.com/olup/kobowriter/matrix\"\n\t\"github.com/olup/kobowriter/screener\"\n\t\"github.com/olup/kobowriter/utils\"\n)\n\nfunc Document(screen *screener.Screen, bus EventBus.Bus, documentPath string) func() {\n\tdocContent := []byte(\"\")\n\tif documentPath != \"\" {\n\t\tdocContent, _ = os.ReadFile(documentPath)\n\t}\n\ttext := &TextView{\n\t\twidth:       int(screen.Width) - 4,\n\t\theight:      int(screen.Height) - 2,\n\t\tcontent:     \"\",\n\t\tscroll:      0,\n\t\tcursorIndex: 0,\n\t}\n\n\ttext.setContent(string(docContent))\n\ttext.setCursorIndex(utils.LenString(string(docContent)))\n\n\tonEvent := func(e event.KeyEvent) {\n\t\tlinesToMove := 1\n\t\tif e.IsCtrl {\n\t\t\tlinesToMove = text.height\n\t\t}\n\n\t\t// if date combo\n\t\tif e.IsChar {\n\t\t\ttext.setContent(utils.InsertAt(text.content, e.KeyChar, text.cursorIndex))\n\t\t\ttext.setCursorIndex(text.cursorIndex + 1)\n\t\t} else {\n\t\t\t// if is modifier key\n\t\t\tswitch e.KeyValue {\n\t\t\tcase \"KEY_BACKSPACE\":\n\t\t\t\ttext.setContent(utils.DeleteAt(text.content, text.cursorIndex))\n\t\t\t\ttext.setCursorIndex(text.cursorIndex - 1)\n\t\t\tcase \"KEY_DEL\":\n\t\t\t\tif text.cursorIndex < utils.LenString(text.content) {\n\t\t\t\t\ttext.setContent(utils.DeleteAt(text.content, text.cursorIndex+1))\n\t\t\t\t}\n\t\t\tcase \"KEY_SPACE\":\n\t\t\t\ttext.setContent(utils.InsertAt(text.content, \" \", text.cursorIndex))\n\t\t\t\ttext.setCursorIndex(text.cursorIndex + 1)\n\t\t\tcase \"KEY_ENTER\":\n\t\t\t\ttext.setContent(utils.InsertAt(text.content, \"\\n\", text.cursorIndex))\n\t\t\t\ttext.setCursorIndex(text.cursorIndex + 1)\n\t\t\tcase \"KEY_RIGHT\":\n\t\t\t\ttext.setCursorIndex(text.cursorIndex + 1)\n\t\t\tcase \"KEY_LEFT\":\n\t\t\t\ttext.setCursorIndex(text.cursorIndex - 1)\n\t\t\tcase \"KEY_DOWN\":\n\t\t\t\ttext.setCursorPos(Position{\n\t\t\t\t\tx: text.cursorPos.x,\n\t\t\t\t\ty: text.cursorPos.y + linesToMove,\n\t\t\t\t})\n\t\t\tcase \"KEY_UP\":\n\t\t\t\ttext.setCursorPos(Position{\n\t\t\t\t\tx: text.cursorPos.x,\n\t\t\t\t\ty: text.cursorPos.y - linesToMove,\n\t\t\t\t})\n\t\t\tcase \"KEY_ESC\":\n\t\t\t\tbus.Publish(\"ROUTING\", \"menu\")\n\t\t\tcase \"KEY_F1\":\n\t\t\t\ttext.setContent(utils.InsertAt(text.content, time.Now().Format(\"02/01/2006\"), text.cursorIndex))\n\t\t\t\ttext.setCursorIndex(text.cursorIndex + 10)\n\n\t\t\tcase \"KEY_F12\":\n\t\t\t\tscreen.RefreshFlash()\n\t\t\t}\n\t\t}\n\n\t\tcompiledMatrix := matrix.PasteMatrix(screen.GetOriginalMatrix(), text.renderMatrix(), 2, 1)\n\t\tscreen.Print(compiledMatrix)\n\n\t\tif documentPath != \"\" {\n\t\t\tos.WriteFile(path.Join(documentPath), []byte(text.content), 0644)\n\t\t}\n\t}\n\n\tbus.SubscribeAsync(\"KEY\", onEvent, false)\n\n\t// display\n\tbus.Publish(\"KEY\", event.KeyEvent{})\n\n\treturn func() {\n\t\tbus.Unsubscribe(\"KEY\", onEvent)\n\t}\n}\n"
  },
  {
    "path": "views/menu.go",
    "content": "package views\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/asaskevich/EventBus\"\n\tgonanoid \"github.com/matoous/go-nanoid/v2\"\n\t\"github.com/olup/kobowriter/event\"\n\t\"github.com/olup/kobowriter/matrix\"\n\t\"github.com/olup/kobowriter/screener\"\n\t\"github.com/olup/kobowriter/utils\"\n)\n\ntype Option struct {\n\tlabel  string\n\taction func()\n}\n\nfunc createMenu(title string, options []Option) func(screen *screener.Screen, bus EventBus.Bus) func() {\n\treturn func(screen *screener.Screen, bus EventBus.Bus) func() {\n\t\tselected := 0\n\t\tonKey := func(e event.KeyEvent) {\n\n\t\t\tif e.KeyValue == \"KEY_UP\" && selected > 0 {\n\t\t\t\tselected--\n\t\t\t}\n\t\t\tif e.KeyValue == \"KEY_DOWN\" && selected < len(options)-1 {\n\t\t\t\tselected++\n\t\t\t}\n\n\t\t\tif e.KeyValue == \"KEY_ENTER\" {\n\t\t\t\toptions[selected].action()\n\t\t\t}\n\n\t\t\tline := 1\n\n\t\t\tmatrixx := screen.GetOriginalMatrix()\n\t\t\tmatrixx = matrix.PasteMatrix(matrixx, matrix.CreateMatrixFromText(title+\"\\n\"+strings.Repeat(\"=\", utils.LenString(title)), utils.LenString(title)), 4, line)\n\n\t\t\tline += 2\n\n\t\t\tfor i, option := range options {\n\t\t\t\toptionMatrix := matrix.CreateMatrixFromText(option.label, utils.LenString(option.label))\n\t\t\t\tif selected == i {\n\t\t\t\t\toptionMatrix = matrix.InverseMatrix(optionMatrix)\n\t\t\t\t}\n\t\t\t\tmatrixx = matrix.PasteMatrix(matrixx, optionMatrix, 4, line+i)\n\t\t\t}\n\n\t\t\tscreen.Print(matrixx)\n\t\t}\n\n\t\tbus.SubscribeAsync(\"KEY\", onKey, false)\n\n\t\t// display\n\t\tbus.Publish(\"KEY\", event.KeyEvent{})\n\n\t\treturn func() {\n\t\t\tbus.Unsubscribe(\"KEY\", onKey)\n\t\t}\n\t}\n}\n\nfunc MainMenu(screen *screener.Screen, bus EventBus.Bus, saveLocation string) func() {\n\toptions := []Option{\n\t\t{\n\t\t\tlabel: \"Back\",\n\t\t\taction: func() {\n\t\t\t\tbus.Publish(\"ROUTING\", \"document\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: \"Export as QR code\",\n\t\t\taction: func() {\n\t\t\t\tbus.Publish(\"ROUTING\", \"qr\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: \"Open Document\",\n\t\t\taction: func() {\n\t\t\t\tbus.Publish(\"ROUTING\", \"file-menu\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: \"New Document\",\n\t\t\taction: func() {\n\t\t\t\tid, _ := gonanoid.New()\n\n\t\t\t\tconfig := utils.LoadConfig(saveLocation)\n\t\t\t\tconfig.LastOpenedDocument = path.Join(saveLocation, id+\".txt\")\n\t\t\t\tutils.SaveConfig(config, saveLocation)\n\n\t\t\t\tbus.Publish(\"ROUTING\", \"document\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: \"Settings\",\n\t\t\taction: func() {\n\t\t\t\tbus.Publish(\"ROUTING\", \"settings-menu\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: \"Quit to XCSoar\",\n\t\t\taction: func() {\n\t\t\t\texec.Command(\"/opt/xcsoar/bin/KoboMenu\").Start()\n\t\t\t\tbus.Publish(\"QUIT\")\n\t\t\t},\n\t\t},\n\t}\n\n\treturn createMenu(\"Menu\", options)(screen, bus)\n}\n\nfunc FileMenu(screen *screener.Screen, bus EventBus.Bus, saveLocation string) func() {\n\tfiles, _ := os.ReadDir(saveLocation)\n\toptions := []Option{\n\t\t{\n\t\t\tlabel: \"Back\",\n\t\t\taction: func() {\n\t\t\t\tbus.Publish(\"ROUTING\", \"menu\")\n\t\t\t},\n\t\t},\n\t}\n\n\tsort.Slice(files, func(i, j int) bool {\n\t\tinfoI, _ := files[i].Info()\n\t\tmodTimeI := infoI.ModTime().Unix()\n\n\t\tinfoJ, _ := files[j].Info()\n\t\tmodTimeJ := infoJ.ModTime().Unix()\n\n\t\treturn modTimeI > modTimeJ\n\t})\n\n\tfor _, file := range files {\n\n\t\tif strings.HasSuffix(file.Name(), \".txt\") {\n\t\t\tfilePath := path.Join(saveLocation, file.Name())\n\t\t\tcontent, _ := os.ReadFile(path.Join(saveLocation, file.Name()))\n\n\t\t\tlabel := strings.Split(string(content), \"\\n\")[0]\n\t\t\tif utils.LenString(label) > 30 {\n\t\t\t\tlabel = string([]rune(label)[0:30]) + \"...\"\n\t\t\t}\n\t\t\toptions = append(options, Option{\n\t\t\t\tlabel: label,\n\n\t\t\t\taction: func() {\n\t\t\t\t\tconfig := utils.LoadConfig(saveLocation)\n\t\t\t\t\tconfig.LastOpenedDocument = filePath\n\t\t\t\t\tutils.SaveConfig(config, saveLocation)\n\n\t\t\t\t\tbus.Publish(\"ROUTING\", \"document\")\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\n\t}\n\n\treturn createMenu(\"Open File\", options)(screen, bus)\n}\n\nfunc SettingsMenu(screen *screener.Screen, bus EventBus.Bus, saveLocation string) func() {\n\toptions := []Option{\n\t\t{\n\t\t\tlabel: \"Back\",\n\t\t\taction: func() {\n\t\t\t\tbus.Publish(\"ROUTING\", \"menu\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tlabel: \"Toggle light\",\n\t\t\taction: func() {\n\t\t\t\tlightPath := \"/sys/class/backlight/mxc_msp430_fl.0/brightness\"\n\t\t\t\tlight := \"0\"\n\t\t\t\tpresentLightRaw, _ := os.ReadFile(lightPath)\n\t\t\t\tpresentLight := strings.TrimSuffix(string(presentLightRaw), \"\\n\")\n\n\t\t\t\tif presentLight == \"0\" {\n\t\t\t\t\tlight = \"10\"\n\t\t\t\t} else {\n\t\t\t\t\tlight = \"0\"\n\t\t\t\t}\n\n\t\t\t\tos.WriteFile(lightPath, []byte(light), os.ModePerm)\n\t\t\t},\n\t\t},\n\t}\n\n\treturn createMenu(\"Open File\", options)(screen, bus)\n}\n"
  },
  {
    "path": "views/qr.go",
    "content": "package views\n\nimport (\n\t\"os\"\n\n\t\"github.com/asaskevich/EventBus\"\n\t\"github.com/olup/kobowriter/event\"\n\t\"github.com/olup/kobowriter/screener\"\n\t\"github.com/olup/kobowriter/utils\"\n\t\"github.com/skip2/go-qrcode\"\n)\n\nfunc Qr(screen *screener.Screen, bus EventBus.Bus, saveLocation string) func() {\n\tonKey := func(event event.KeyEvent) {\n\t\tscreen.Clear()\n\t\tbus.Publish(\"ROUTING\", \"menu\")\n\t}\n\n\tbus.SubscribeAsync(\"KEY\", onKey, false)\n\n\t// Display QR on mount\n\tscreen.Clear()\n\tconfig := utils.LoadConfig(saveLocation)\n\tcontent, err := os.ReadFile(config.LastOpenedDocument)\n\tif err != nil {\n\t\tbus.Publish(\"ROUTING\", \"menu\")\n\t}\n\n\timage, _ := qrcode.Encode(string(content), qrcode.High, 800)\n\n\tscreen.PrintPng(image, 800, 800, 100, 100)\n\n\treturn func() {\n\t\tbus.Unsubscribe(\"KEY\", onKey)\n\t}\n}\n"
  },
  {
    "path": "views/textView.go",
    "content": "package views\n\nimport (\n\t\"strings\"\n\t\"unicode/utf8\"\n\n\t\"github.com/olup/kobowriter/matrix\"\n\t\"github.com/olup/kobowriter/utils\"\n)\n\ntype TextView struct {\n\tcontent     string\n\twidth       int\n\theight      int\n\twrapContent []string\n\tcursorIndex int\n\tcursorPos   Position\n\tlineCount   []int\n\tscroll      int\n}\n\ntype Position struct {\n\tx int\n\ty int\n}\n\nfunc (t *TextView) init(width int) {\n\tt.width = width\n}\n\nfunc (t *TextView) setContent(text string) {\n\tt.content = text\n\tt.wrapContent = strings.Split(utils.WrapText(text, t.width), \"\\n\")\n\n\tlineCount := []int{}\n\tfor _, line := range t.wrapContent {\n\t\tlineCount = append(lineCount, utf8.RuneCountInString(line)+1)\n\t}\n\tt.lineCount = lineCount\n}\n\nfunc (t *TextView) setCursorIndex(index int) {\n\n\t// Bounds\n\tif index < 0 {\n\t\tindex = 0\n\t}\n\tif index > utils.LenString(t.content) {\n\t\tindex = utils.LenString(t.content)\n\t}\n\n\t// Processing\n\tt.cursorIndex = index\n\tx := 0\n\ty := 0\n\n\tagg := 0\n\n\tfor i, count := range t.lineCount {\n\t\taggNext := count + agg\n\t\tif aggNext > t.cursorIndex {\n\t\t\ty = i\n\t\t\tx = t.cursorIndex - agg\n\t\t\tbreak\n\t\t}\n\t\tagg = aggNext\n\t}\n\n\tt.cursorPos = Position{\n\t\tx,\n\t\ty,\n\t}\n\n\tt.updateScroll()\n\n}\n\nfunc (t *TextView) setCursorPos(position Position) {\n\t// Bounds\n\tif position.y < 0 {\n\t\tposition.y = 0\n\t}\n\n\tif position.x < 0 {\n\t\tposition.x = 0\n\t}\n\n\tif position.y > len(t.lineCount)-1 {\n\t\tposition.y = len(t.lineCount) - 1\n\t}\n\n\tif t.lineCount[position.y]-1 < position.x {\n\t\tposition.x = t.lineCount[position.y] - 1\n\t}\n\n\t// Procesing\n\n\tagg := 0\n\n\tfor i := 0; i < position.y; i++ {\n\t\tagg += t.lineCount[i]\n\t}\n\n\tagg += position.x\n\n\tt.cursorPos = position\n\tt.cursorIndex = agg\n\tt.updateScroll()\n\n}\n\nfunc (t *TextView) renderMatrix() matrix.Matrix {\n\ttextMatrix := matrix.CreateMatrixFromText(t.content, t.width)\n\tif t.cursorPos.x >= 0 && t.cursorPos.y >= 0 && t.cursorPos.x < t.width {\n\t\ttextMatrix[t.cursorPos.y][t.cursorPos.x].IsInverted = true\n\t}\n\tendBound := t.scroll + t.height\n\tif endBound > len(textMatrix) {\n\t\tendBound = len(textMatrix)\n\t}\n\tscrolledTextMatrix := textMatrix[t.scroll:endBound]\n\treturn scrolledTextMatrix\n}\n\nfunc (t *TextView) updateScroll() {\n\ty := t.cursorPos.y\n\n\tif y > t.scroll+t.height-1 {\n\t\tt.scroll = y - 5\n\t}\n\tif y < t.scroll {\n\t\tt.scroll = y - t.height + 5\n\t}\n\tif t.scroll > len(t.wrapContent) {\n\t\tt.scroll = len(t.wrapContent) - 5\n\t}\n\tif t.scroll < 0 {\n\t\tt.scroll = 0\n\t}\n}\n"
  }
]