11 Commits

17 changed files with 1130 additions and 22 deletions

View File

@@ -12,6 +12,7 @@
### Features
- Notification relay
- Notificstion transliteration
- Music control
- Get info from watch (HRM, Battery level, Firmware version)
- Set current time
@@ -36,6 +37,42 @@ The various request types and their data requirements can be seen in `internal/t
---
### Transliteration
Since the PineTime does not have enough space to store all unicode glyphs, it only stores the ASCII space and Cyrillic. Therefore, this daemon can transliterate unsupported characters into supported ones. Since some languages have different transliterations, the transliterators to be used must be specified in the config. Here are the available transliterators:
- eASCII
- Scandinavian
- German
- Hebrew
- Greek
- Russian
- Ukranian
- Arabic
- Farsi
- Polish
- Lithuanian
- Estonian
- Icelandic
- Czeck
- French
- Armenian
- Korean
- Chinese
- Emoji
Place the desired map names in an array as `notifs.translit.use`. They will be evaluated in order. You can also put custom transliterations in `notifs.translit.custom`. These take priority over any other maps. The `notifs.translit` config section should look like this:
```toml
[notifs.translit]
use = ["eASCII", "Russian", "Emoji"]
custom = [
"test", "replaced"
]
```
---
### `itctl`
This daemon comes with a binary called `itctl` which uses the socket to control the daemon from the command line. As such, it can be scripted using bash.
@@ -61,6 +98,30 @@ Flags:
Use "itctl [command] --help" for more information about a command.
```
---
### `itgui`
In `cmd/itgui`, there is a gui frontend to the socket of `itd`. It uses the [fyne library](https://fyne.io/) for Go. It can be compiled by running:
```shell
go build ./cmd/itgui
```
#### Screenshots
![Info tab](https://i.imgur.com/okxG9EI.png)
![Notify tab](https://i.imgur.com/DrVhOAq.png)
![Set time tab](https://i.imgur.com/j9civeY.png)
![Upgrade tab](https://i.imgur.com/1KY6fG4.png)
![Upgrade in progress](https://i.imgur.com/w5qbWAw.png)
---
#### Interactive mode
Running `itctl` by itself will open interactive mode. It's essentially a shell where you can enter commands. For example:

View File

@@ -95,14 +95,14 @@ var upgradeCmd = &cobra.Command{
if err != nil {
log.Fatal().Err(err).Msg("Error decoding response data")
}
// If transfer finished, break
if event.Received == event.Total {
break
}
// Set total bytes in progress bar
bar.SetTotal(event.Total)
// Set amount of bytes received in progress bar
bar.SetCurrent(event.Received)
// If transfer finished, break
if event.Received == event.Total {
break
}
}
// Finish progress bar
bar.Finish()

View File

@@ -2,6 +2,7 @@ package main
import (
"image/color"
"os"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
@@ -10,7 +11,7 @@ import (
"fyne.io/fyne/v2/widget"
)
func guiErr(err error, msg string, parent fyne.Window) {
func guiErr(err error, msg string, fatal bool, parent fyne.Window) {
// Create new label containing message
msgLbl := widget.NewLabel(msg)
// Text formatting settings
@@ -33,6 +34,20 @@ func guiErr(err error, msg string, parent fyne.Window) {
widget.NewAccordionItem("More Details", errLbl),
))
}
// Show error dialog
dialog.NewCustom("Error", "Ok", content, parent).Show()
if fatal {
// Create new error dialog
errDlg := dialog.NewCustom("Error", "Close", content, parent)
// On close, exit with code 1
errDlg.SetOnClosed(func() {
os.Exit(1)
})
// Show dialog
errDlg.Show()
// Run app prematurely to stop further execution
parent.ShowAndRun()
} else {
// Show error dialog
dialog.NewCustom("Error", "Ok", content, parent).Show()
}
}

View File

@@ -58,7 +58,7 @@ func infoTab(parent fyne.Window) *fyne.Container {
fwVerString, err := get(types.ReqTypeFwVersion)
if err != nil {
panic(err)
guiErr(err, "Error getting firmware string", true, parent)
}
fwVer := container.NewVBox(
@@ -99,7 +99,7 @@ func watch(req int, onRecv func(data interface{}), parent fyne.Window) error {
for scanner.Scan() {
res, err := getResp(scanner.Bytes())
if err != nil {
guiErr(err, "Error getting response from connection", parent)
guiErr(err, "Error getting response from connection", false, parent)
continue
}
onRecv(res.Value)

View File

@@ -1,6 +1,8 @@
package main
import (
"net"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
)
@@ -13,6 +15,11 @@ func main() {
// Create new window with title "itgui"
window := a.NewWindow("itgui")
_, err := net.Dial("unix", SockPath)
if err != nil {
guiErr(err, "Error dialing itd socket", true, window)
}
// Create new app tabs container
tabs := container.NewAppTabs(
container.NewTabItem("Info", infoTab(window)),

View File

@@ -25,7 +25,7 @@ func notifyTab(parent fyne.Window) *fyne.Container {
// Dial itd UNIX socket
conn, err := net.Dial("unix", SockPath)
if err != nil {
guiErr(err, "Error dialing socket", parent)
guiErr(err, "Error dialing socket", false, parent)
return
}
// Encode notify request on connection

View File

@@ -29,7 +29,7 @@ func timeTab(parent fyne.Window) *fyne.Container {
// Parse time as RFC1123 string
parsedTime, err := time.Parse(time.RFC1123, timeEntry.Text)
if err != nil {
guiErr(err, "Error parsing time string", parent)
guiErr(err, "Error parsing time string", false, parent)
return
}
// Set time to parsed time

View File

@@ -92,7 +92,7 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
// If archive path does not exist and both init packet and firmware paths
// also do not exist, return error
if archivePath == "" && (initPktPath == "" && fiwmarePath == "") {
guiErr(nil, "Upgrade requires archive or files selected", parent)
guiErr(nil, "Upgrade requires archive or files selected", false, parent)
return
}
@@ -125,7 +125,7 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
// Dial itd UNIX socket
conn, err := net.Dial("unix", SockPath)
if err != nil {
guiErr(err, "Error dialing socket", parent)
guiErr(err, "Error dialing socket", false, parent)
return
}
defer conn.Close()
@@ -150,24 +150,20 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
// Decode scanned line into response struct
err = json.Unmarshal(scanner.Bytes(), &res)
if err != nil {
guiErr(err, "Error decoding response", parent)
guiErr(err, "Error decoding response", false, parent)
return
}
if res.Error {
guiErr(err, "Error returned in response", parent)
guiErr(err, "Error returned in response", false, parent)
return
}
var event types.DFUProgress
// Decode response data into progress struct
err = mapstructure.Decode(res.Value, &event)
if err != nil {
guiErr(err, "Error decoding response value", parent)
guiErr(err, "Error decoding response value", false, parent)
return
}
// If transfer finished, break
if event.Received == event.Total {
break
}
// Set label text to received / total B
progressLbl.SetText(fmt.Sprintf("%d / %d B", event.Received, event.Total))
// Set progress bar values
@@ -175,6 +171,10 @@ func upgradeTab(parent fyne.Window) *fyne.Container {
progressBar.Value = float64(event.Received)
// Refresh progress bar
progressBar.Refresh()
// If transfer finished, break
if event.Received == event.Total {
break
}
}
})

3
go.mod
View File

@@ -15,6 +15,7 @@ require (
github.com/mattn/go-isatty v0.0.13 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mitchellh/mapstructure v1.4.1
github.com/mozillazg/go-pinyin v0.18.0
github.com/rs/zerolog v1.23.0
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/cast v1.4.1 // indirect
@@ -22,5 +23,5 @@ require (
github.com/spf13/viper v1.8.1
go.arsenm.dev/infinitime v0.0.0-20210825051734-745b4bd37cf4
golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/text v0.3.7
)

2
go.sum
View File

@@ -242,6 +242,8 @@ github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mozillazg/go-pinyin v0.18.0 h1:hQompXO23/0ohH8YNjvfsAITnCQImCiR/Fny8EhIeW0=
github.com/mozillazg/go-pinyin v0.18.0/go.mod h1:iR4EnMMRXkfpFVV5FMi4FNB6wGq9NV6uDWbUuPhP4Yc=
github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d h1:EG/xyWjHT19rkUpwsWSkyiCCmyqNwFovr9m10rhyOxU=
github.com/muka/go-bluetooth v0.0.0-20210812063148-b6c83362e27d/go.mod h1:dMCjicU6vRBk34dqOmIZm0aod6gUwZXOXzBROqGous0=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=

View File

@@ -15,6 +15,9 @@ cfg.version = 2
notify = true
setTime = true
[notifs.translit]
use = ["eASCII", "Russian", "Emoji"]
[notifs.ignore]
sender = []
summary = ["InfiniTime"]

View File

@@ -25,6 +25,7 @@ import (
"github.com/rs/zerolog/log"
"github.com/spf13/viper"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/translit"
)
func initNotifRelay(dev *infinitime.Device) error {
@@ -71,6 +72,12 @@ func initNotifRelay(dev *infinitime.Device) error {
continue
}
maps := viper.GetStringSlice("notifs.translit.use")
translit.Transliterators["custom"] = translit.Map(viper.GetStringSlice("notifs.translit.custom"))
sender = translit.Transliterate(sender, maps...)
summary = translit.Transliterate(summary, maps...)
body = translit.Transliterate(body, maps...)
var msg string
// If summary does not exist, set message to body.
// If it does, set message to summary, two newlines, and then body

View File

@@ -32,6 +32,7 @@ import (
"github.com/spf13/viper"
"go.arsenm.dev/infinitime"
"go.arsenm.dev/itd/internal/types"
"go.arsenm.dev/itd/translit"
)
func startSocket(dev *infinitime.Device) error {
@@ -169,8 +170,12 @@ func handleConnection(conn net.Conn, dev *infinitime.Device) {
connErr(conn, err, "Error decoding request data")
break
}
maps := viper.GetStringSlice("notifs.translit.use")
translit.Transliterators["custom"] = translit.Map(viper.GetStringSlice("notifs.translit.custom"))
title := translit.Transliterate(reqData.Title, maps...)
body := translit.Transliterate(reqData.Body, maps...)
// Send notification to watch
err = dev.Notify(reqData.Title, reqData.Body)
err = dev.Notify(title, body)
if err != nil {
connErr(conn, err, "Error sending notification")
break

138
translit/armenian.go Normal file
View File

@@ -0,0 +1,138 @@
package translit
import (
"strings"
)
type ArmenianTranslit struct{}
var armenianMap = []string{
"աու", "au",
"բու", "bu",
"գու", "gu",
"դու", "du",
"եու", "eu",
"զու", "zu",
"էու", "eu",
"ըու", "yu",
"թու", "tu",
"ժու", "ju",
"իու", "iu",
"լու", "lu",
"խու", "xu",
"ծու", "cu",
"կու", "ku",
"հու", "hu",
"ձու", "dzu",
"ղու", "xu",
"ճու", "cu",
"մու", "mu",
"յու", "yu",
"նու", "nu",
"շու", "shu",
"չու", "chu",
"պու", "pu",
"ջու", "ju",
"ռու", "ru",
"սու", "su",
"վու", "vu",
"տու", "tu",
"րու", "ru",
"ցու", "cu",
"փու", "pu",
"քու", "qu",
"օու", "ou",
"ևու", "eu",
"ֆու", "fu",
"ոու", "vou",
"ու", "u",
"բո", "bo",
"գո", "go",
"դո", "do",
"զո", "zo",
"թո", "to",
"ժո", "jo",
"լո", "lo",
"խո", "xo",
"ծո", "co",
"կո", "ko",
"հո", "ho",
"ձո", "dzo",
"ղո", "xo",
"ճո", "co",
"մո", "mo",
"յո", "yo",
"նո", "no",
"շո", "so",
"չո", "co",
"պո", "po",
"ջո", "jo",
"ռո", "ro",
"սո", "so",
"վո", "vo",
"տո", "to",
"րո", "ro",
"ցո", "co",
"փո", "po",
"քո", "qo",
"ևո", "eo",
"ֆո", "fo",
"ո", "vo",
"եւ", "ev",
"եվ", "ev",
"ա", "a",
"բ", "b",
"գ", "g",
"դ", "d",
"ե", "e",
"զ", "z",
"է", "e",
"ը", "y",
"թ", "t",
"ժ", "j",
"ի", "i",
"լ", "l",
"խ", "x",
"ծ", "c",
"կ", "k",
"հ", "h",
"ձ", "dz",
"ղ", "x",
"ճ", "c",
"մ", "m",
"յ", "y",
"ն", "n",
"շ", "sh",
"չ", "ch",
"պ", "p",
"ջ", "j",
"ռ", "r",
"ս", "s",
"վ", "v",
"տ", "t",
"ր", "r",
"ց", "c",
"փ", "p",
"ք", "q",
"օ", "o",
"և", "ev",
"ֆ", "f",
"ւ", "",
}
func init() {
lower := armenianMap
for i, val := range lower {
if i%2 == 1 {
continue
}
capital := strings.Title(val)
if capital != val {
armenianMap = append(armenianMap, capital, strings.Title(armenianMap[i+1]))
}
}
}
func (at *ArmenianTranslit) Transliterate(s string) string {
return strings.NewReplacer(armenianMap...).Replace(s)
}

42
translit/chinese.go Normal file
View File

@@ -0,0 +1,42 @@
package translit
import (
"bytes"
"strings"
"unicode"
"github.com/mozillazg/go-pinyin"
)
// ChineseTranslit implements Transliterator using a pinyin
// conversion library.
type ChineseTranslit struct{}
func (ct *ChineseTranslit) Transliterate(s string) string {
// Create buffer for final output
outBuf := &bytes.Buffer{}
// Create buffer to temporarily store chinese characters
tmpBuf := &bytes.Buffer{}
// For every character in string
for _, char := range s {
// If character in Han range
if unicode.Is(unicode.Han, char) {
// Write character to temporary buffer
tmpBuf.WriteRune(char)
} else {
// If buffer contains characters
if tmpBuf.Len() > 0 {
// Convert to pinyin (without tones)
out := pinyin.LazyConvert(tmpBuf.String(), nil)
// Write space-separated string to output
outBuf.WriteString(strings.Join(out, " "))
// Reset temporary buffer
tmpBuf.Reset()
}
// Write character to output
outBuf.WriteRune(char)
}
}
// Return output string
return outBuf.String()
}

470
translit/korean.go Normal file
View File

@@ -0,0 +1,470 @@
package translit
import (
"strings"
"unicode"
"golang.org/x/text/unicode/norm"
)
// https://en.wikipedia.org/wiki/Hangul_Jamo_%28Unicode_block%29
var jamoBlock = &unicode.RangeTable{
R16: []unicode.Range16{{
Lo: 0x1100,
Hi: 0x11FF,
Stride: 1,
}},
}
// https://en.wikipedia.org/wiki/Hangul_Syllables
var syllablesBlock = &unicode.RangeTable{
R16: []unicode.Range16{{
Lo: 0xAC00,
Hi: 0xD7A3,
Stride: 1,
}},
}
// https://en.wikipedia.org/wiki/Hangul_Compatibility_Jamo
var compatJamoBlock = &unicode.RangeTable{
R16: []unicode.Range16{{
Lo: 0x3131,
Hi: 0x318E,
Stride: 1,
}},
}
// KoreanTranslit implements transliteration for Korean.
//
// This was translated to Go from the code in https://codeberg.org/Freeyourgadget/Gadgetbridge
type KoreanTranslit struct{}
// User input consisting of isolated jamo is usually mapped to the KS X 1001 compatibility
// block, but jamo resulting from decomposed syllables are mapped to the modern one. This
// function maps compat jamo to modern ones where possible and returns all other characters
// unmodified.
//
// https://en.wikipedia.org/wiki/Hangul_Compatibility_Jamo
// https://en.wikipedia.org/wiki/Hangul_Jamo_%28Unicode_block%29
func decompatJamo(jamo rune) rune {
// KS X 1001 Hangul filler, not used in modern Unicode. A useful landmark in the
// compatibility jamo block.
// https://en.wikipedia.org/wiki/KS_X_1001#Hangul_Filler
var hangulFiller rune = 0x3164
// Ignore characters outside compatibility jamo block
if !unicode.In(jamo, compatJamoBlock) {
return jamo
}
// Vowels are contiguous, in the same order, and unambiguous so it's a simple offset.
if jamo >= 0x314F && jamo < hangulFiller {
return jamo - 0x1FEE
}
// Consonants are organized differently. No clean way to do this.
// The compatibility jamo block doesn't distinguish between Choseong (leading) and Jongseong
// (final) positions, but the modern block does. We map to Choseong here.
switch jamo {
case 0x3131:
return 0x1100 // ㄱ
case 0x3132:
return 0x1101 // ㄲ
case 0x3134:
return 0x1102 // ㄴ
case 0x3137:
return 0x1103 // ㄷ
case 0x3138:
return 0x1104 // ㄸ
case 0x3139:
return 0x1105 // ㄹ
case 0x3141:
return 0x1106 // ㅁ
case 0x3142:
return 0x1107 // ㅂ
case 0x3143:
return 0x1108 // ㅃ
case 0x3145:
return 0x1109 // ㅅ
case 0x3146:
return 0x110A // ㅆ
case 0x3147:
return 0x110B // ㅇ
case 0x3148:
return 0x110C // ㅈ
case 0x3149:
return 0x110D // ㅉ
case 0x314A:
return 0x110E // ㅊ
case 0x314B:
return 0x110F // ㅋ
case 0x314C:
return 0x1110 // ㅌ
case 0x314D:
return 0x1111 // ㅍ
case 0x314E:
return 0x1112 // ㅎ
}
// The rest of the compatibility block consists of archaic compounds that are
// unlikely to be encountered in modern systems. Just leave them alone.
return jamo
}
// Transliterates one jamo at a time.
// Does nothing if it isn't in the modern jamo block.
func translitSingleJamo(jamo rune) string {
jamo = decompatJamo(jamo)
switch jamo {
// Choseong (leading position consonants)
case 0x1100:
return "g" // ㄱ
case 0x1101:
return "kk" // ㄲ
case 0x1102:
return "n" // ㄴ
case 0x1103:
return "d" // ㄷ
case 0x1104:
return "tt" // ㄸ
case 0x1105:
return "r" // ㄹ
case 0x1106:
return "m" // ㅁ
case 0x1107:
return "b" // ㅂ
case 0x1108:
return "pp" // ㅃ
case 0x1109:
return "s" // ㅅ
case 0x110A:
return "ss" // ㅆ
case 0x110B:
return "" // ㅇ
case 0x110C:
return "j" // ㅈ
case 0x110D:
return "jj" // ㅉ
case 0x110E:
return "ch" // ㅊ
case 0x110F:
return "k" // ㅋ
case 0x1110:
return "t" // ㅌ
case 0x1111:
return "p" // ㅍ
case 0x1112:
return "h" // ㅎ
// Jungseong (vowels)
case 0x1161:
return "a" // ㅏ
case 0x1162:
return "ae" // ㅐ
case 0x1163:
return "ya" // ㅑ
case 0x1164:
return "yae" // ㅒ
case 0x1165:
return "eo" // ㅓ
case 0x1166:
return "e" // ㅔ
case 0x1167:
return "yeo" // ㅕ
case 0x1168:
return "ye" // ㅖ
case 0x1169:
return "o" // ㅗ
case 0x116A:
return "wa" // ㅘ
case 0x116B:
return "wae" // ㅙ
case 0x116C:
return "oe" // ㅚ
case 0x116D:
return "yo" // ㅛ
case 0x116E:
return "u" // ㅜ
case 0x116F:
return "wo" // ㅝ
case 0x1170:
return "we" // ㅞ
case 0x1171:
return "wi" // ㅟ
case 0x1172:
return "yu" // ㅠ
case 0x1173:
return "eu" // ㅡ
case 0x1174:
return "ui" // ㅢ
case 0x1175:
return "i" // ㅣ
// Jongseong (final position consonants)
case 0x11A8:
return "k" // ㄱ
case 0x11A9:
return "k" // ㄲ
case 0x11AB:
return "n" // ㄴ
case 0x11AE:
return "t" // ㄷ
case 0x11AF:
return "l" // ㄹ
case 0x11B7:
return "m" // ㅁ
case 0x11B8:
return "p" // ㅂ
case 0x11BA:
return "t" // ㅅ
case 0x11BB:
return "t" // ㅆ
case 0x11BC:
return "ng" // ㅇ
case 0x11BD:
return "t" // ㅈ
case 0x11BE:
return "t" // ㅊ
case 0x11BF:
return "k" // ㅋ
case 0x11C0:
return "t" // ㅌ
case 0x11C1:
return "p" // ㅍ
case 0x11C2:
return "t" // ㅎ
}
return string(jamo)
}
// Some combinations of ending jamo in one syllable and initial jamo in the next are romanized
// irregularly. These exceptions are called "special provisions". In cases where multiple
// romanizations are permitted, we use the one that's least commonly used elsewhere.
//
// Returns empty strring and false if either character is not in the modern jamo block,
// or if there is no special provision for that pair of jamo.
func translitSpecialProvisions(previousEnding rune, nextInitial rune) (string, bool) {
// Return false if previousEnding not in modern jamo block
if !unicode.In(previousEnding, jamoBlock) {
return "", false
}
// Return false if nextInitial not in modern jamo block
if !unicode.In(nextInitial, jamoBlock) {
return "", false
}
// Jongseong (final position) ㅎ has a number of special provisions.
if previousEnding == 0x11C2 {
switch nextInitial {
case 0x110B:
return "h", true // ㅇ
case 0x1100:
return "k", true // ㄱ
case 0x1102:
return "nn", true // ㄴ
case 0x1103:
return "t", true // ㄷ
case 0x1105:
return "nn", true // ㄹ
case 0x1106:
return "nm", true // ㅁ
case 0x1107:
return "p", true // ㅂ
case 0x1109:
return "hs", true // ㅅ
case 0x110C:
return "ch", true // ㅈ
case 0x1112:
return "t", true // ㅎ
default:
return "", false
}
}
// Otherwise, special provisions are denser when grouped by the second jamo.
switch nextInitial {
case 0x1100: // ㄱ
switch previousEnding {
case 0x11AB:
return "n-g", true // ㄴ
default:
return "", false
}
case 0x1102: // ㄴ
switch previousEnding {
case 0x11A8:
return "ngn", true // ㄱ
case 0x11AE:
fallthrough // ㄷ
case 0x11BA:
fallthrough // ㅅ
case 0x11BD:
fallthrough // ㅈ
case 0x11BE:
fallthrough // ㅊ
case 0x11C0: // ㅌ
return "nn", true
case 0x11AF:
return "ll", true // ㄹ
case 0x11B8:
return "mn", true // ㅂ
default:
return "", false
}
case 0x1105: // ㄹ
switch previousEnding {
case 0x11A8:
fallthrough // ㄱ
case 0x11AB:
fallthrough // ㄴ
case 0x11AF: // ㄹ
return "ll", true
case 0x11AE:
fallthrough // ㄷ
case 0x11BA:
fallthrough // ㅅ
case 0x11BD:
fallthrough // ㅈ
case 0x11BE:
fallthrough // ㅊ
case 0x11C0: // ㅌ
return "nn", true
case 0x11B7:
fallthrough // ㅁ
case 0x11B8: // ㅂ
return "mn", true
case 0x11BC:
return "ngn", true // ㅇ
default:
return "", false
}
case 0x1106: // ㅁ
switch previousEnding {
case 0x11A8:
return "ngm", true // ㄱ
case 0x11AE:
fallthrough // ㄷ
case 0x11BA:
fallthrough // ㅅ
case 0x11BD:
fallthrough // ㅈ
case 0x11BE:
fallthrough // ㅊ
case 0x11C0: // ㅌ
return "nm", true
case 0x11B8:
return "mm", true // ㅂ
default:
return "", false
}
case 0x110B: // ㅇ
switch previousEnding {
case 0x11A8:
return "g", true // ㄱ
case 0x11AE:
return "d", true // ㄷ
case 0x11AF:
return "r", true // ㄹ
case 0x11B8:
return "b", true // ㅂ
case 0x11BA:
return "s", true // ㅅ
case 0x11BC:
return "ng-", true // ㅇ
case 0x11BD:
return "j", true // ㅈ
case 0x11BE:
return "ch", true // ㅊ
default:
return "", false
}
case 0x110F: // ㅋ
switch previousEnding {
case 0x11A8:
return "k-k", true // ㄱ
default:
return "", false
}
case 0x1110: // ㅌ
switch previousEnding {
case 0x11AE:
fallthrough // ㄷ
case 0x11BA:
fallthrough // ㅅ
case 0x11BD:
fallthrough // ㅈ
case 0x11BE:
fallthrough // ㅊ
case 0x11C0: // ㅌ
return "t-t", true
default:
return "", false
}
case 0x1111: // ㅍ
switch previousEnding {
case 0x11B8:
return "p-p", true // ㅂ
default:
return "", false
}
default:
return "", false
}
}
// Decompose a syllable into several jamo. Does nothing if that isn't possible.
func decompose(syllable rune) string {
return norm.NFD.String(string(syllable))
}
// Transliterate any Hangul in the given string.
// Leaves any non-Hangul characters unmodified.
func (kt *KoreanTranslit) Transliterate(s string) string {
if len(s) == 0 {
return s
}
builder := &strings.Builder{}
nextInitialJamoConsumed := false
for i, syllable := range s {
// If character not in blocks, leave it unmodified
if !unicode.In(syllable, jamoBlock, syllablesBlock, compatJamoBlock) {
builder.WriteRune(syllable)
continue
}
jamo := decompose(syllable)
for j, char := range jamo {
// If we already transliterated the first jamo of this syllable as part of a special
// provision, skip it. Otherwise, handle it in the unconditional else branch.
if j == 0 && nextInitialJamoConsumed {
nextInitialJamoConsumed = false
continue
}
// If this is the last jamo of this syllable and not the last syllable of the
// string, check for special provisions. If the next char is whitespace or not
// Hangul, run translitSpecialProvisions() should return no value.
if j == len(jamo)-1 && i < len(s)-1 {
nextSyllable := s[i+1]
nextJamo := decompose(rune(nextSyllable))[0]
// Attempt to handle special provision
specialProvision, ok := translitSpecialProvisions(char, rune(nextJamo))
if ok {
builder.WriteString(specialProvision)
nextInitialJamoConsumed = true
} else {
// Not a special provision, transliterate normally
builder.WriteString(translitSingleJamo(char))
}
continue
}
// Transliterate normally
builder.WriteString(translitSingleJamo(char))
}
}
return builder.String()
}

357
translit/translit.go Normal file
View File

@@ -0,0 +1,357 @@
package translit
import (
"strings"
)
// Transliterate runs the given maps on s and returns the result
func Transliterate(s string, useMaps ...string) string {
// Create variable to store modified string
out := s
// If custom map exists
if customMap, ok := Transliterators["custom"]; ok {
// Perform transliteration with it
out = customMap.Transliterate(out)
}
// For every map to use
for _, useMap := range useMaps {
// If custom, skip
if useMap == "custom" {
continue
}
// Get requested map
translitMap, ok := Transliterators[useMap]
if !ok {
continue
}
// Perform transliteration
out = translitMap.Transliterate(out)
}
// Return result
return out
}
// Transliterator is implemented by anything with a
// Transliterate method, which performs transliteration
// and returns the resulting string.
type Transliterator interface {
Transliterate(string) string
}
// Map implements Transliterator using a slice where
// every odd element is a key and every even one is a value
// which replaces the key.
type Map []string
func (mt Map) Transliterate(s string) string {
return strings.NewReplacer(mt...).Replace(s)
}
// Transliterators stores transliterator implementations for each supported language.
// Some of these were sourced from https://codeberg.org/Freeyourgadget/Gadgetbridge
var Transliterators = map[string]Transliterator{
"eASCII": Map{
"œ", "oe",
"ª", "a",
"°", "o",
"«", `"`,
"»", `"`,
},
"Scandinavian": Map{
"Æ", "Ae",
"æ", "ae",
"Ø", "Oe",
"ø", "oe",
"Å", "Aa",
"å", "aa",
},
"German": Map{
"ä", "ae",
"ö", "oe",
"ü", "ue",
"Ä", "Ae",
"Ö", "Oe",
"Ü", "Ue",
"ß", "ss",
"ẞ", "SS",
},
"Hebrew": Map{
"א", "a",
"ב", "b",
"ג", "g",
"ד", "d",
"ה", "h",
"ו", "u",
"ז", "z",
"ח", "kh",
"ט", "t",
"י", "y",
"כ", "c",
"ל", "l",
"מ", "m",
"נ", "n",
"ס", "s",
"ע", "'",
"פ", "p",
"צ", "ts",
"ק", "k",
"ר", "r",
"ש", "sh",
"ת", "th",
"ף", "f",
"ץ", "ts",
"ך", "ch",
"ם", "m",
"ן", "n",
},
"Greek": Map{
"α", "a",
"ά", "a",
"β", "v",
"γ", "g",
"δ", "d",
"ε", "e",
"έ", "e",
"ζ", "z",
"η", "i",
"ή", "i",
"θ", "th",
"ι", "i",
"ί", "i",
"ϊ", "i",
"ΐ", "i",
"κ", "k",
"λ", "l",
"μ", "m",
"ν", "n",
"ξ", "ks",
"ο", "o",
"ό", "o",
"π", "p",
"ρ", "r",
"σ", "s",
"ς", "s",
"τ", "t",
"υ", "y",
"ύ", "y",
"ϋ", "y",
"ΰ", "y",
"φ", "f",
"χ", "ch",
"ψ", "ps",
"ω", "o",
"ώ", "o",
"Α", "A",
"Ά", "A",
"Β", "B",
"Γ", "G",
"Δ", "D",
"Ε", "E",
"Έ", "E",
"Ζ", "Z",
"Η", "I",
"Ή", "I",
"Θ", "TH",
"Ι", "I",
"Ί", "I",
"Ϊ", "I",
"Κ", "K",
"Λ", "L",
"Μ", "M",
"Ν", "N",
"Ξ", "KS",
"Ο", "O",
"Ό", "O",
"Π", "P",
"Ρ", "R",
"Σ", "S",
"Τ", "T",
"Υ", "Y",
"Ύ", "Y",
"Ϋ", "Y",
"Φ", "F",
"Χ", "CH",
"Ψ", "PS",
"Ω", "O",
"Ώ", "O",
},
"Russian": Map{
"Ё", "Йo",
"ё", "йo",
},
"Ukranian": Map{
"ґ", "gh",
"є", "je",
"і", "i",
"ї", "ji",
"Ґ", "GH",
"Є", "JE",
"І", "I",
"Ї", "JI",
},
"Arabic": Map{
"ا", "a",
"ب", "b",
"ت", "t",
"ث", "th",
"ج", "j",
"ح", "7",
"خ", "5",
"د", "d",
"ذ", "th",
"ر", "r",
"ز", "z",
"س", "s",
"ش", "sh",
"ص", "9",
"ض", "9'",
"ط", "6",
"ظ", "6'",
"ع", "3",
"غ", "3'",
"ف", "f",
"ق", "q",
"ك", "k",
"ل", "l",
"م", "m",
"ن", "n",
"ه", "h",
"و", "w",
"ي", "y",
"ى", "a",
"ﺓ", "",
"آ", "2",
"ئ", "2",
"إ", "2",
"ؤ", "2",
"أ", "2",
"ء", "2",
"٠", "0",
"١", "1",
"٢", "2",
"٣", "3",
"٤", "4",
"٥", "5",
"٦", "6",
"٧", "7",
"٨", "8",
"٩", "9",
},
"Farsi": Map{
"پ", "p",
"چ", "ch",
"ژ", "zh",
"ک", "k",
"گ", "g",
"ی", "y",
"\u200c", " ",
"؟", "?",
"٪", "%",
"؛", ";",
"،", ":",
"۱", "1",
"۲", "2",
"۳", "3",
"۴", "4",
"۵", "5",
"۶", "6",
"۷", "7",
"۸", "8",
"۹", "9",
"۰", "0",
"»", "<",
"«", ">",
"ِ", "e",
"َ", "a",
"ُ", "o",
"ّ", "",
},
"Polish": Map{
"Ł", "L",
"ł", "l",
},
"Lithuanian": Map{
"ą", "a",
"č", "c",
"ę", "e",
"ė", "e",
"į", "i",
"š", "s",
"ų", "u",
"ū", "u",
"ž", "z",
},
"Estonian": Map{
"ä", "a",
"Ä", "A",
"ö", "o",
"õ", "o",
"Ö", "O",
"Õ", "O",
"ü", "u",
"Ü", "U",
},
"Icelandic": Map{
"Þ", "Th",
"þ", "th",
"Ð", "D",
"ð", "d",
},
"Czeck": Map{
"ř", "r",
"ě", "e",
"ý", "y",
"á", "a",
"í", "i",
"é", "e",
"ó", "o",
"ú", "u",
"ů", "u",
"ď", "d",
"ť", "t",
"ň", "n",
},
"French": Map{
"à", "a",
"â", "a",
"é", "e",
"è", "e",
"ê", "e",
"ë", "e",
"ù", "u",
"ü", "u",
"ÿ", "y",
"ç", "c",
},
"Emoji": Map{
"😂", ":')",
"😊", ":)",
"😃", ":)",
"😩", "-_-",
"😏", ":J",
"💜", "<3",
"💖", "<3",
"💗", "<3",
"❤️", "<3",
"💕", "<3",
"💞", "<3",
"💘", "<3",
"💓", "<3",
"💚", "<3",
"💙", "<3",
"💔", "</3",
"😱", "D:",
"😮", ":O",
"😝", ":P",
"😍", ":x",
"😢", ":(",
"💯", ":100:",
"🔥", ":fire:",
"😉", ";)",
"😴", ":zzz:",
"💤", ":zzz:",
},
"Korean": &KoreanTranslit{},
"Chinese": &ChineseTranslit{},
"Armenian": &ArmenianTranslit{},
}