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
================================================
<p align="center">
<img src="assets/kobowriter.png" />
</p>
# 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
}
}
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
SYMBOL INDEX (52 symbols across 12 files)
FILE: event/keyEvents.go
type KeyEvent (line 9) | type KeyEvent struct
function BindKeyEvent (line 21) | func BindKeyEvent(k *keylogger.KeyLogger, b EventBus.Bus) {
FILE: findKeyboard.go
function findKeyboard (line 15) | func findKeyboard(screen *screener.Screen, bus EventBus.Bus) {
FILE: main.go
function main (line 19) | func main() {
FILE: matrix/matrix.go
type MatrixElement (line 9) | type MatrixElement struct
type Matrix (line 14) | type Matrix
function CreateNewMatrix (line 16) | func CreateNewMatrix(width int, height int) Matrix {
function CreateMatrixFromText (line 30) | func CreateMatrixFromText(text string, width int) Matrix {
function PasteMatrix (line 48) | func PasteMatrix(baseMatrix Matrix, topMatrix Matrix, offsetX int, offse...
function MatrixToText (line 70) | func MatrixToText(matrix Matrix) string {
function InverseMatrix (line 80) | func InverseMatrix(in Matrix) (out Matrix) {
function FillMatrix (line 90) | func FillMatrix(in Matrix, char rune) (out Matrix) {
function CopyMatrix (line 100) | func CopyMatrix(in Matrix) (out Matrix) {
FILE: screener/image.go
function getPixels (line 10) | func getPixels(file io.Reader) ([]byte, error) {
function getPixelsFromImage (line 34) | func getPixelsFromImage(img image.Image) ([]byte, error) {
FILE: screener/screen.go
type Screen (line 13) | type Screen struct
method Clean (line 68) | func (s *Screen) Clean() {
method Print (line 72) | func (s *Screen) Print(matrix matrix.Matrix) {
method PrintPng (line 128) | func (s *Screen) PrintPng(imgBytes []byte, w int, h int, x int, y int) {
method PrintAlert (line 150) | func (s *Screen) PrintAlert(message string, width int) {
method Clear (line 160) | func (s *Screen) Clear() {
method ClearFlash (line 165) | func (s *Screen) ClearFlash() {
method RefreshFlash (line 170) | func (s *Screen) RefreshFlash() {
method GetOriginalMatrix (line 176) | func (s *Screen) GetOriginalMatrix() matrix.Matrix {
function InitScreen (line 27) | func InitScreen() (s *Screen) {
function same (line 77) | func same(a matrix.MatrixElement, b matrix.MatrixElement) bool {
function printDiff (line 81) | func printDiff(previous matrix.Matrix, next matrix.Matrix, fb *gofbink.F...
function getCharImage (line 134) | func getCharImage(s string) []byte {
FILE: utils/text.go
function WrapLine (line 5) | func WrapLine(text string, lineWidth int) (wrapped string) {
function WrapText (line 29) | func WrapText(text string, lineWidth int) string {
FILE: utils/utils.go
type Config (line 13) | type Config struct
function LoadConfig (line 17) | func LoadConfig(saveLocation string) Config {
function SaveConfig (line 35) | func SaveConfig(config Config, saveLocation string) {
function IsLetter (line 43) | func IsLetter(s string) bool {
function InsertAt (line 47) | func InsertAt(text string, insert string, index int) string {
function DeleteAt (line 55) | func DeleteAt(text string, index int) string {
function LenString (line 60) | func LenString(s string) int {
FILE: views/document.go
function Document (line 15) | func Document(screen *screener.Screen, bus EventBus.Bus, documentPath st...
FILE: views/menu.go
type Option (line 18) | type Option struct
function createMenu (line 23) | func createMenu(title string, options []Option) func(screen *screener.Sc...
function MainMenu (line 68) | func MainMenu(screen *screener.Screen, bus EventBus.Bus, saveLocation st...
function FileMenu (line 118) | func FileMenu(screen *screener.Screen, bus EventBus.Bus, saveLocation st...
function SettingsMenu (line 167) | func SettingsMenu(screen *screener.Screen, bus EventBus.Bus, saveLocatio...
FILE: views/qr.go
function Qr (line 13) | func Qr(screen *screener.Screen, bus EventBus.Bus, saveLocation string) ...
FILE: views/textView.go
type TextView (line 11) | type TextView struct
method init (line 27) | func (t *TextView) init(width int) {
method setContent (line 31) | func (t *TextView) setContent(text string) {
method setCursorIndex (line 42) | func (t *TextView) setCursorIndex(index int) {
method setCursorPos (line 78) | func (t *TextView) setCursorPos(position Position) {
method renderMatrix (line 112) | func (t *TextView) renderMatrix() matrix.Matrix {
method updateScroll (line 125) | func (t *TextView) updateScroll() {
type Position (line 22) | type Position struct
Condensed preview — 18 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (39K chars).
[
{
"path": ".gitignore",
"chars": 10,
"preview": "kobowriter"
},
{
"path": "Makefile",
"chars": 128,
"preview": ".PHONY: build\nbuild: \n\tCGO_ENABLED=1 GOARCH=arm GOOS=linux CC=${CROSS_TC}-gcc CXX=${CROSS_TC}-g++ go build -o ./build/ko"
},
{
"path": "README.md",
"chars": 4034,
"preview": "<p align=\"center\">\n <img src=\"assets/kobowriter.png\" />\n</p>\n\n# Kobowriter\n\nThis small project aims to let you use your"
},
{
"path": "event/key.go",
"chars": 2259,
"preview": "//AZERTYkeybindings\n\npackage event\n\nvar KeyCode = map[int]string{\n\t0: \"KEY_RESERVED\",\n\t1: \"KEY_ESC\",\n\n\t2: \"&\",\n\t3: \"é\""
},
{
"path": "event/keyEvents.go",
"chars": 1828,
"preview": "package event\n\nimport (\n\t\"github.com/MarinX/keylogger\"\n\t\"github.com/asaskevich/EventBus\"\n\t\"github.com/olup/kobowriter/ut"
},
{
"path": "findKeyboard.go",
"chars": 1055,
"preview": "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\"gi"
},
{
"path": "go.mod",
"chars": 502,
"preview": "module github.com/olup/kobowriter\n\ngo 1.16\n\nrequire (\n\tgithub.com/MarinX/keylogger v0.0.0-20210528193429-a54d7834cc1a\n\tg"
},
{
"path": "go.sum",
"chars": 2939,
"preview": "github.com/MarinX/keylogger v0.0.0-20210528193429-a54d7834cc1a h1:ItKXWegGGThcahUf+ylKFa5pwqkRJofaOyeGdzwO2mM=\ngithub.co"
},
{
"path": "main.go",
"chars": 1743,
"preview": "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/sc"
},
{
"path": "matrix/matrix.go",
"chars": 2144,
"preview": "package matrix\n\nimport (\n\t\"strings\"\n\n\t\"github.com/olup/kobowriter/utils\"\n)\n\ntype MatrixElement struct {\n\tContent rune"
},
{
"path": "screener/image.go",
"chars": 981,
"preview": "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.R"
},
{
"path": "screener/screen.go",
"chars": 4496,
"preview": "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\"g"
},
{
"path": "utils/text.go",
"chars": 718,
"preview": "package utils\n\nimport \"strings\"\n\nfunc WrapLine(text string, lineWidth int) (wrapped string) {\n\tif text == \"\" {\n\t\treturn "
},
{
"path": "utils/utils.go",
"chars": 1401,
"preview": "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-nano"
},
{
"path": "views/document.go",
"chars": 2603,
"preview": "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\"g"
},
{
"path": "views/menu.go",
"chars": 4265,
"preview": "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"
},
{
"path": "views/qr.go",
"chars": 779,
"preview": "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/k"
},
{
"path": "views/textView.go",
"chars": 2351,
"preview": "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/ut"
}
]
About this extraction
This page contains the full source code of the olup/kobowriter GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 18 files (33.4 KB), approximately 11.7k tokens, and a symbol index with 52 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.