├── .gitignore ├── LICENSE ├── README.md ├── ai1.png ├── aih.gif ├── bard.go ├── build.sh ├── chatgpt.go ├── claude.go ├── claude.goo ├── else ├── README.md- ├── bard.py ├── chatgpt.py ├── claude2.py ├── dev.go ├── eng │ ├── go.mod │ ├── go.sum │ └── playphrase.go ├── ggt.go ├── huggingchat.py └── thought.txt ├── falcon180.go ├── go.mod ├── go.sum ├── llama2.go ├── main.go ├── release.sh ├── release └── upload.go ├── ryy ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── action.go ├── buffer.go ├── clipboard.go ├── commands.go ├── config.go ├── go.mod ├── go.sum ├── highlighting.go ├── hooks.go ├── init.go ├── key.go ├── mark.go ├── mode_term.go ├── mode_visual.go ├── modes.go ├── prompt.go ├── render.go ├── ry.go ├── ryy ├── screenshot.png ├── search.go ├── styles.go ├── terminal │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── color.go │ ├── csi.go │ ├── csi_test.go │ ├── doc.go │ ├── ioctl.go │ ├── parse.go │ ├── state.go │ ├── str.go │ ├── str_test.go │ ├── vt.go │ └── vt_test.go ├── utils.go └── view.go └── vi.go /.gitignore: -------------------------------------------------------------------------------- 1 | aih 2 | mvi 3 | vi 4 | aih.json 5 | aih.mov 6 | .history 7 | .quest.txt 8 | .aih.json 9 | .mvi 10 | .vi 11 | release/* 12 | !release/upload.go 13 | history.txt 14 | test 15 | test/* 16 | ryy/.git 17 | ryy/.gitignore 18 | tmp 19 | tmp/* 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 ashahChen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Talk to AI modes in terminal. 2 | 3 | screenshot 4 | 5 | ## Usage 6 | Download [binary file](https://github.com/Databingo/aih/releases) then type: 7 | ```bash 8 | ./aih 9 | ``` 10 | ## Command list 11 | | Command | Operation| 12 | |------------|----------| 13 | |. | Select AI mode of Bard/ChatGPT/Claude2/Llama2/Falcon180 | 14 | |↑ | Previous input| 15 | |↓ | Next input| 16 | |<< | Start multiple lines input mode| 17 | |>> | End multiple lines input mode| 18 | |j | Scroll down| 19 | |k | Scroll up| 20 | |f | Page down| 21 | |p | Page up| 22 | |g | Scroll to top| 23 | |G | Scroll to bottom| 24 | |q or Enter | Back to conversation| 25 | |.v | Mini `vi` to edit quest, `:ai` send, `:q` cancel| 26 | |.c or .clear| Clear the screen| 27 | |.h or .history | Show history of conversations| 28 | |.r or .restart | Re-start AI model| 29 | |.proxy | Set proxy, for example: socks5://127.0.0.1:7890| 30 | |.help | Show help| 31 | |.exit | Exit Aih| 32 | 33 | ## Prerequisites 34 | - [Chrome Browser](https://google.com/chrome) 35 | - Free account of [Bard](https://bard.google.com), [Claude](https://claude.ai), [OpenAI](https://chat.openai.com), [HuggingChat](https://huggingface.co/chat) logged-in manually on your Chrome browser. 36 | - (Optional) Paid ChatGPT API on [Billing](https://platform.openai.com/account/billing/overview). 37 | 38 | ## Tips 39 | - Close Chrome browser before run Aih at the first time. 40 | - More usage of command [Liner](https://github.com/peterh/liner#line-editing). 41 | - More usage of mini [vi](https://github.com/kiasaki/ry#features). 42 | - Answer will be auotmatically saved in system clipboard for pasting. 43 | - Conversations were persisted in `history.txt` beside Aih binary. 44 | - `./aih -rod=show` to monitor in browser. 45 | - All-In-One mode will display answers from all the AI modes. 46 | screenshot 47 | 48 | ## Supported OS 49 | - Mac/Linux/Windows 50 | 51 | ## Installation 52 | ``` 53 | $ git clone https://github.com/Databingo/aih 54 | $ go clean -cache && go clean -modcache 55 | $ cd aih/ryy && go mod tidy && go build -o ../vi && cd .. 56 | $ go mod tidy && go build -tags vi 57 | ``` 58 | ## Acknowledgements 59 | - github.com/rivo/tview 60 | - github.com/peterh/liner 61 | - github.com/gdamore/tcell/v2 62 | - github.com/atotto/clipboard 63 | - github.com/go-rod/rod 64 | - github.com/go-rod/stealth 65 | - github.com/tidwall/gjson 66 | - github.com/tidwall/sjson 67 | - github.com/manifoldco/promptui 68 | - github.com/sashabaranov/go-openai 69 | - github.com/kiasaki/ry 70 | 71 | ## Todo 72 | - x for disable certain AI mode. 73 | - Change deamon mode to minimal browser mode for passing bot checking of ChatGPT. 74 | 75 | ## License 76 | MIT and "You can choose not to include this MIT license into your copies". 77 | -------------------------------------------------------------------------------- /ai1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Databingo/aih/31676d2ffa7fd0758f37d8e5b21ea464900fa215/ai1.png -------------------------------------------------------------------------------- /aih.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Databingo/aih/31676d2ffa7fd0758f37d8e5b21ea464900fa215/aih.gif -------------------------------------------------------------------------------- /bard.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "github.com/go-rod/rod" 7 | "github.com/go-rod/stealth" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | 13 | // Set up client of Bard (Rod version) 14 | var page_bard *rod.Page 15 | var relogin_bard = true 16 | var channel_bard chan string 17 | 18 | func Bard() { 19 | channel_bard = make(chan string) 20 | defer func() { 21 | if err := recover(); err != nil { 22 | relogin_bard = true 23 | } 24 | }() 25 | page_bard = stealth.MustPage(browser) 26 | page_bard.MustNavigate("https://bard.google.com") 27 | 28 | for i := 1; i <= 30; i++ { 29 | //if page_bard.MustHasX("//textarea[@id='mat-input-0']") { 30 | //if page_bard.MustHasX("//rich-textarea[@aria-label='Input for prompt text']") { 31 | if page_bard.MustHasX("//rich-textarea[@enterkeyhint='send']") { 32 | relogin_bard = false 33 | break 34 | } 35 | // Check "I'm not a robot" 36 | info := page_bard.MustInfo() 37 | if strings.HasPrefix(info.URL, "https://google.com/sorry") { 38 | relogin_bard = true 39 | break 40 | } 41 | // Check "Sign in" 42 | if page_bard.MustHasX("//a[contains(text(), 'Sign in')]") { 43 | relogin_bard = true 44 | break 45 | } 46 | // Check "You've been signed out" 47 | if page_bard.MustHasX("//h1[contains(text(), 've been signed out')]") { 48 | relogin_bard = true 49 | break 50 | } 51 | 52 | time.Sleep(time.Second) 53 | } 54 | if relogin_bard == true { 55 | sprint("✘ Bard") 56 | } 57 | if relogin_bard == false { 58 | sprint("✔ Bard") 59 | for { 60 | select { 61 | case question := <-channel_bard: 62 | page_bard.MustActivate() 63 | //page_bard.MustElementX("//rich-textarea[@aria-label='Input for prompt text']").MustWaitVisible().MustInput(question) 64 | page_bard.MustElementX("//rich-textarea[@enterkeyhint='send']").MustWaitVisible().MustInput(question) 65 | page_bard.MustElementX("//button[@mattooltip='Submit']").MustClick() 66 | fmt.Println("Bard generating...", role) 67 | //page_bard.MustActivate() 68 | if role == ".all" { 69 | //fmt.Println("Bard role", role) 70 | channel_bard <- "click_bard" 71 | } 72 | // wait generated icon 73 | var generated_icon_appear = false 74 | var c = 0 75 | for i := 1; i <= 60; i++ { 76 | if page_bard.MustHasX("//img[contains(@src, 'https://www.gstatic.com/lamda/images/sparkle_resting_v2_1ff6f6a71f2d298b1a31.gif')]") { 77 | generated_icon_appear = true 78 | break 79 | } 80 | c = c + 1 81 | if c == 5 { 82 | page_bard.MustActivate() 83 | c = 0 84 | } 85 | time.Sleep(1 * time.Second) 86 | } 87 | if generated_icon_appear == true { 88 | img := page_bard.MustElementX("//img[contains(@src, 'https://www.gstatic.com/lamda/images/sparkle_resting_v2_1ff6f6a71f2d298b1a31.gif')][last()]").MustWaitVisible() 89 | //response := img.MustElementX("parent::div/parent::bard-logo/parent::div/parent::div").MustWaitVisible() 90 | //response := img.MustElementX("parent::div/parent::div/parent::bard-avatar/parent::div/parent::div").MustWaitVisible() 91 | response := img.MustElementX("ancestor::bard-avatar[1]/parent::div/parent::div").MustWaitVisible() 92 | answer := response.MustText() 93 | channel_bard <- answer 94 | } else { 95 | channel_bard <- "✘✘ Bard, Please check the internet connection and verify login status." 96 | relogin_bard = true 97 | 98 | } 99 | } 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | cd ryy && go build -o ../vi && cd - 2 | go build -tags vi 3 | -------------------------------------------------------------------------------- /chatgpt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "github.com/go-rod/rod" 7 | "github.com/go-rod/stealth" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | // Set up client of chatgpt (rod version) 13 | var page_chatgpt *rod.Page 14 | var relogin_chatgpt = true 15 | var channel_chatgpt chan string 16 | 17 | func Chatgpt() { 18 | channel_chatgpt = make(chan string) 19 | defer func() { 20 | if err := recover(); err != nil { 21 | relogin_chatgpt = true 22 | } 23 | }() 24 | //page_chatgpt = browser.MustPage("https://chat.openai.com") 25 | page_chatgpt = stealth.MustPage(browser) 26 | page_chatgpt.MustNavigate("https://chat.openai.com") 27 | 28 | channel_chatgpt_tips := make(chan string) 29 | go func() { 30 | for i := 1; i <= 30; i++ { 31 | if page_chatgpt.MustHasX("//div[contains(text(), 'Okay, let')]") { 32 | page_chatgpt.MustElementX("//div[contains(text(), 'Okay, let')]").MustWaitVisible().MustClick() 33 | close(channel_chatgpt_tips) 34 | break 35 | } 36 | if page_chatgpt.MustHasX("//h2[contains(text(), 'Your session has expired')]") { 37 | relogin_chatgpt = true 38 | close(channel_chatgpt_tips) 39 | break 40 | } 41 | time.Sleep(time.Second) 42 | } 43 | }() 44 | 45 | //for i := 1; i <= 30; i++ { 46 | // if page_chatgpt.MustHasX("//div[contains(text(), 'Okay, let')]") { 47 | // page_chatgpt.MustElementX("//div[contains(text(), 'Okay, let')]").MustWaitVisible().MustClick() 48 | // } 49 | // time.Sleep(time.Second) 50 | //} 51 | //for i := 1; i <= 30; i++ { 52 | // if page_chatgpt.MustHasX("//h2[contains(text(), 'Your session has expired')]") { 53 | // relogin_chatgpt = true 54 | // break 55 | // } 56 | // if page_chatgpt.MustHasX("//div[contains(text(), 'Something went wrong. If this issue persists please')]") { 57 | // //fmt.Println("ChatGPT web error") 58 | // //channel_chatgpt <- "✘✘ ChatGPT, Please check the internet connection and verify login status." 59 | // relogin_chatgpt = true 60 | // break 61 | // //page_chatgpt.MustPDF("ChatGPT✘.pdf") 62 | // } 63 | // time.Sleep(time.Second) 64 | //} 65 | for i := 1; i <= 30; i++ { 66 | if page_chatgpt.MustHasX("//textarea[@id='prompt-textarea']") && !page_chatgpt.MustHasX("//h2[contains(text(), 'Your session has expired')]") { 67 | relogin_chatgpt = false 68 | break 69 | } 70 | time.Sleep(time.Second) 71 | } 72 | 73 | if relogin_chatgpt == true { 74 | sprint("✘ ChatGPT") 75 | //page_chatgpt.MustPDF("./tmp/ChatGPT✘.pdf") 76 | } 77 | if relogin_chatgpt == false { 78 | sprint("✔ ChatGPT") 79 | for { 80 | select { 81 | case question := <-channel_chatgpt: 82 | page_chatgpt.MustActivate() 83 | for i := 1; i <= 20; i++ { 84 | if page_chatgpt.MustHasX("//textarea[@id='prompt-textarea']") { 85 | page_chatgpt.MustElementX("//textarea[@id='prompt-textarea']").MustInput(question) 86 | break 87 | } 88 | time.Sleep(1 * time.Second) 89 | } 90 | for i := 1; i <= 20; i++ { 91 | if page_chatgpt.MustHasX("//textarea[@id='prompt-textarea']/..//button") { 92 | page_chatgpt.MustElementX("//textarea[@id='prompt-textarea']/..//button").MustClick() 93 | break 94 | } 95 | time.Sleep(1 * time.Second) 96 | } 97 | fmt.Println("ChatGPT generating...", role) 98 | //page_chatgpt.MustActivate() 99 | if role == ".all" { 100 | channel_chatgpt <- "click_chatgpt" 101 | } 102 | 103 | 104 | //page_chatgpt.MustElementX("//div[contains(text(), 'Stop generating')]") 105 | //page_chatgpt.MustElement("svg path[d='M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2z']") 106 | //fmt.Println("found creating icon") 107 | var regenerate_icon = false 108 | 109 | for i := 1; i <= 60; i++ { 110 | //if page_chatgpt.MustHas("svg path[d='M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15']") { 111 | if page_chatgpt.MustHas("svg path[d='M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2z']") { 112 | 113 | time.Sleep(1 * time.Second) 114 | continue 115 | } 116 | 117 | if page_chatgpt.MustHas("svg path[d='M4.5 2.5C5.05228 2.5 5.5 2.94772 5.5 3.5V5.07196C7.19872 3.47759 9.48483 2.5 12 2.5C17.2467 2.5 21.5 6.75329 21.5 12C21.5 17.2467 17.2467 21.5 12 21.5C7.1307 21.5 3.11828 17.8375 2.565 13.1164C2.50071 12.5679 2.89327 12.0711 3.4418 12.0068C3.99033 11.9425 4.48712 12.3351 4.5514 12.8836C4.98798 16.6089 8.15708 19.5 12 19.5C16.1421 19.5 19.5 16.1421 19.5 12C19.5 7.85786 16.1421 4.5 12 4.5C9.7796 4.5 7.7836 5.46469 6.40954 7H9C9.55228 7 10 7.44772 10 8C10 8.55228 9.55228 9 9 9H4.5C3.96064 9 3.52101 8.57299 3.50073 8.03859C3.49983 8.01771 3.49958 7.99677 3.5 7.9758V3.5C3.5 2.94772 3.94771 2.5 4.5 2.5Z']") { 118 | //page_chatgpt.MustElement("svg path[d='M7 11L12 6L17 11M12 18V7']").MustWaitVisible().MustWaitStable() 119 | if page_chatgpt.MustHas("svg path[d='M7 11L12 6L17 11M12 18V7']") { 120 | regenerate_icon = true 121 | //fmt.Println("wait...") 122 | break 123 | } 124 | } 125 | time.Sleep(1 * time.Second) 126 | } 127 | if regenerate_icon == true { 128 | page_chatgpt.MustActivate() 129 | time.Sleep(3 * time.Second) 130 | 131 | answer := page_chatgpt.MustElementX("(//div[contains(@class, 'w-full text-token-text-primary')])[last()]").MustText()[15:] 132 | //answer := page_chatgpt.MustElementX("(//div[contains(@class, 'group w-full')])[last()]").MustText()[7:] 133 | //answer := page_chatgpt.MustElementX("(//div[contains(@class, 'group final-completion w-full')])[last()]").MustText()[7:] 134 | //answer_div := page_chatgpt.MustElementX("(//div[contains(@class, 'w-full text-token-text-primary')])[last()]").MustWaitStable() 135 | //answer := answer_div.MustText()[15:] 136 | 137 | //response := img.MustElementX("ancestor::bard-avatar[1]/parent::div/parent::div").MustWaitVisible() 138 | //answer := response.MustText() 139 | 140 | if strings.Contains(answer, 141 | "An error occurred. Either the engine you requested does not exist or there was another issue processing your request. If this issue persists please contact us through our help center at help.openai.com.") { 142 | relogin_chatgpt = true 143 | } 144 | channel_chatgpt <- answer 145 | } else { 146 | channel_chatgpt <- "✘✘ ChatGPT, Please check the internet connection and verify login status." 147 | relogin_chatgpt = true 148 | //page_chatgpt.MustPDF("./tmp/ChatGPT✘.pdf") 149 | 150 | } 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /claude.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "github.com/go-rod/rod" 7 | //"github.com/go-rod/stealth" 8 | "github.com/google/uuid" 9 | "github.com/tidwall/gjson" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | var page_claude *rod.Page 16 | var relogin_claude = true 17 | var channel_claude chan string 18 | 19 | func Claude2() { 20 | channel_claude = make(chan string) 21 | defer func() { 22 | if err := recover(); err != nil { 23 | relogin_claude = true 24 | } 25 | }() 26 | page_claude = browser.MustPage("https://claude.ai") 27 | //page_claude = stealth.MustPage(browser) 28 | //page_claude.MustNavigate("https://claude.ai/chats").MustWaitLoad() 29 | for i := 1; i <= 30; i++ { 30 | if page_claude.MustHasX("//div[contains(text(), 'Start a new chat')]") { 31 | time.Sleep(3 * time.Second) 32 | page_claude.MustElementX("//div[contains(text(), 'Start a new chat')]").MustClick() 33 | relogin_claude = false 34 | break 35 | } 36 | time.Sleep(time.Second) 37 | } 38 | 39 | 40 | 41 | org_json := page_claude.MustElementX("//pre").MustText() 42 | org_uuid := gjson.Get(string(org_json), "0.uuid").String() 43 | time.Sleep(6 * time.Second) 44 | 45 | new_uuid := uuid.New().String() 46 | new_uuid_url := "https://claude.ai/api/organizations/" + org_uuid + "/chat_conversations" 47 | create_new_converastion_json := `{"uuid":"` + new_uuid + `","name":""}` 48 | create_new_converastion_js := ` 49 | (new_uuid_url, sdata) => { 50 | var xhr = new XMLHttpRequest(); 51 | xhr.open("POST", new_uuid_url); 52 | xhr.setRequestHeader('Content-Type', 'application/json'); 53 | xhr.setRequestHeader('Referer', 'https://claude.ai/chats'); 54 | xhr.setRequestHeader('Origin', 'https://claude.ai'); 55 | xhr.setRequestHeader('TE', 'trailers'); 56 | xhr.setRequestHeader('DNT', '1'); 57 | xhr.setRequestHeader('Connection', 'keep-alive'); 58 | xhr.setRequestHeader('Accept', 'text/event-stream, text/event-stream'); 59 | xhr.onreadystatechange = function() { 60 | if (xhr.readyState == XMLHttpRequest.DONE) { 61 | var res_text = xhr.responseText; 62 | console.log(res_text); 63 | } 64 | } 65 | console.log(sdata); 66 | xhr.send(sdata); 67 | } 68 | ` 69 | page_claude.MustEval(create_new_converastion_js, new_uuid_url, create_new_converastion_json).Str() 70 | // posted new conversation uuid 71 | time.Sleep(3 * time.Second) // delay to simulate human being 72 | 73 | var record_chat_messages string 74 | var response_chat_messages string 75 | for i := 1; i <= 20; i++ { 76 | create_json := page_claude.MustNavigate("https://claude.ai/api/organizations/" + org_uuid + "/chat_conversations/" + new_uuid).MustElementX("//pre").MustText() 77 | 78 | message_uuid := gjson.Get(string(create_json), "uuid").String() 79 | if message_uuid == new_uuid { 80 | //fmt.Println("create_conversation success...") 81 | relogin_claude = false 82 | record_chat_messages = gjson.Get(string(create_json), "chat_messages").String() 83 | break 84 | } 85 | time.Sleep(2 * time.Second) 86 | } 87 | 88 | if relogin_claude == true { 89 | sprint("✘ Claude") 90 | //page_claude.MustPDF("./tmp/Claude✘.pdf") 91 | } 92 | if relogin_claude == false { 93 | sprint("✔ Claude") 94 | for { 95 | select { 96 | case question := <-channel_claude: 97 | question = strings.Replace(question, "\r", "\n", -1) 98 | question = strings.Replace(question, "\"", "\\\"", -1) 99 | question = strings.Replace(question, "\n", "\\n", -1) 100 | question = strings.TrimSuffix(question, "\n") 101 | // re-activate 102 | page_claude.MustNavigate("https://claude.ai/api/account/statsig/" + org_uuid).MustWaitLoad() 103 | record_json := page_claude.MustNavigate("https://claude.ai/api/organizations/" + org_uuid + "/chat_conversations/" + new_uuid).MustElementX("//pre").MustText() 104 | record_chat_messages = gjson.Get(string(record_json), "chat_messages").String() 105 | record_count := gjson.Get(string(response_chat_messages), "#").Int() 106 | page_claude.MustNavigate("https://claude.ai/api/organizations/" + org_uuid).MustWaitLoad() 107 | time.Sleep(3 * time.Second) // delay to simulate human being 108 | //question = strings.Replace(question, `"`, `\"`, -1) // escape " in input text when code into json 109 | 110 | //d := `{"completion":{"prompt":"` + question + `","timezone":"Asia/Shanghai","model":"claude-2"},"organization_uuid":"` + org_uuid + `","conversation_uuid":"` + new_uuid + `","text":"` + question + `","attachments":[]}` 111 | d := `{"prompt":"` + question + `","timezone":"Asia/Bangkok","model":"claude-2.1","attachments":[],"files":[]}` 112 | //fmt.Println(d) 113 | //js := ` 114 | // (sdata, new_uuid) => { 115 | // var xhr = new XMLHttpRequest(); 116 | // xhr.open("POST", "https://claude.ai/api/append_message"); 117 | // xhr.setRequestHeader('Content-Type', 'application/json'); 118 | // xhr.setRequestHeader('Referer', 'https://claude.ai/chat/new_uuid'); 119 | // xhr.setRequestHeader('Origin', 'https://claude.ai'); 120 | // xhr.setRequestHeader('TE', 'trailers'); 121 | // xhr.setRequestHeader('Connection', 'keep-alive'); 122 | // xhr.setRequestHeader('Accept', 'text/event-stream, text/event-stream'); 123 | // xhr.onreadystatechange = function() { 124 | // if (xhr.readyState == XMLHttpRequest.DONE) { 125 | // var res_text = xhr.responseText; 126 | // console.log(res_text); 127 | // } 128 | // } 129 | // console.log(sdata); 130 | // xhr.send(sdata); 131 | // } 132 | // ` 133 | js := ` 134 | (sdata, org_uuid, new_uuid) => { 135 | var xhr = new XMLHttpRequest(); 136 | xhr.open("POST", "https://claude.ai/api/organizations/org_uuid/chat_conversations/new_uuid/completion"); 137 | xhr.setRequestHeader('Content-Type', 'application/json'); 138 | xhr.setRequestHeader('Referer', 'https://claude.ai/chat/new_uuid'); 139 | xhr.setRequestHeader('Origin', 'https://claude.ai'); 140 | xhr.setRequestHeader('TE', 'trailers'); 141 | xhr.setRequestHeader('Connection', 'keep-alive'); 142 | xhr.setRequestHeader('Accept', 'text/event-stream, text/event-stream'); 143 | xhr.onreadystatechange = function() { 144 | if (xhr.readyState == XMLHttpRequest.DONE) { 145 | var res_text = xhr.responseText; 146 | console.log(res_text); 147 | } 148 | } 149 | console.log(sdata); 150 | xhr.send(sdata); 151 | } 152 | ` 153 | page_claude.MustEval(js, d, org_uuid, new_uuid).Str() 154 | fmt.Println("Claude generating...", role) 155 | if role == ".all" { 156 | channel_claude <- "click_claude" 157 | } 158 | time.Sleep(3 * time.Second) // delay to simulate human being 159 | 160 | // wait answer 161 | var claude_response = false 162 | var response_json string 163 | for i := 1; i <= 20; i++ { 164 | if page_claude.MustNavigate("https://claude.ai/api/organizations/" + org_uuid + "/chat_conversations/" + new_uuid).MustHasX("//pre") { 165 | response_json = page_claude.MustNavigate("https://claude.ai/api/organizations/" + org_uuid + "/chat_conversations/" + new_uuid).MustElementX("//pre").MustText() 166 | response_chat_messages = gjson.Get(string(response_json), "chat_messages").String() 167 | count := gjson.Get(string(response_chat_messages), "#").Int() 168 | 169 | if response_chat_messages != record_chat_messages && count == record_count+2 { 170 | claude_response = true 171 | record_chat_messages = response_chat_messages 172 | break 173 | } 174 | } 175 | time.Sleep(3 * time.Second) 176 | } 177 | if claude_response == true { 178 | count := gjson.Get(string(response_chat_messages), "#").Int() 179 | answer := gjson.Get(string(response_json), "chat_messages.#(index=="+strconv.FormatInt(count-1, 10)+").text").String() 180 | channel_claude <- answer 181 | } else { 182 | channel_claude <- "✘✘ Claude, Please check the internet connection and verify login status." 183 | relogin_claude = true 184 | //page_claude.MustPDF("./tmp/Claude✘.pdf") 185 | } 186 | } 187 | } 188 | } 189 | 190 | } 191 | -------------------------------------------------------------------------------- /claude.goo: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "github.com/go-rod/rod" 7 | "github.com/go-rod/stealth" 8 | "github.com/google/uuid" 9 | "github.com/tidwall/gjson" 10 | "strconv" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | var page_claude *rod.Page 16 | var relogin_claude = true 17 | var channel_claude chan string 18 | 19 | func Claude2() { 20 | channel_claude = make(chan string) 21 | defer func() { 22 | if err := recover(); err != nil { 23 | relogin_claude = true 24 | } 25 | }() 26 | //page_claude = browser.MustPage("https://claude.ai") 27 | page_claude = stealth.MustPage(browser) 28 | page_claude.MustNavigate("https://claude.ai/api/organizations").MustWaitLoad() 29 | org_json := page_claude.MustElementX("//pre").MustText() 30 | org_uuid := gjson.Get(string(org_json), "0.uuid").String() 31 | time.Sleep(6 * time.Second) 32 | 33 | new_uuid := uuid.New().String() 34 | new_uuid_url := "https://claude.ai/api/organizations/" + org_uuid + "/chat_conversations" 35 | create_new_converastion_json := `{"uuid":"` + new_uuid + `","name":""}` 36 | create_new_converastion_js := ` 37 | (new_uuid_url, sdata) => { 38 | var xhr = new XMLHttpRequest(); 39 | xhr.open("POST", new_uuid_url); 40 | xhr.setRequestHeader('Content-Type', 'application/json'); 41 | xhr.setRequestHeader('Referer', 'https://claude.ai/chats'); 42 | xhr.setRequestHeader('Origin', 'https://claude.ai'); 43 | xhr.setRequestHeader('TE', 'trailers'); 44 | xhr.setRequestHeader('DNT', '1'); 45 | xhr.setRequestHeader('Connection', 'keep-alive'); 46 | xhr.setRequestHeader('Accept', 'text/event-stream, text/event-stream'); 47 | xhr.onreadystatechange = function() { 48 | if (xhr.readyState == XMLHttpRequest.DONE) { 49 | var res_text = xhr.responseText; 50 | console.log(res_text); 51 | } 52 | } 53 | console.log(sdata); 54 | xhr.send(sdata); 55 | } 56 | ` 57 | page_claude.MustEval(create_new_converastion_js, new_uuid_url, create_new_converastion_json).Str() 58 | // posted new conversation uuid 59 | time.Sleep(3 * time.Second) // delay to simulate human being 60 | 61 | var record_chat_messages string 62 | var response_chat_messages string 63 | for i := 1; i <= 20; i++ { 64 | create_json := page_claude.MustNavigate("https://claude.ai/api/organizations/" + org_uuid + "/chat_conversations/" + new_uuid).MustElementX("//pre").MustText() 65 | 66 | message_uuid := gjson.Get(string(create_json), "uuid").String() 67 | if message_uuid == new_uuid { 68 | //fmt.Println("create_conversation success...") 69 | relogin_claude = false 70 | record_chat_messages = gjson.Get(string(create_json), "chat_messages").String() 71 | break 72 | } 73 | time.Sleep(2 * time.Second) 74 | } 75 | 76 | if relogin_claude == true { 77 | sprint("✘ Claude") 78 | //page_claude.MustPDF("./tmp/Claude✘.pdf") 79 | } 80 | if relogin_claude == false { 81 | sprint("✔ Claude") 82 | for { 83 | select { 84 | case question := <-channel_claude: 85 | question = strings.Replace(question, "\r", "\n", -1) 86 | question = strings.Replace(question, "\"", "\\\"", -1) 87 | question = strings.Replace(question, "\n", "\\n", -1) 88 | question = strings.TrimSuffix(question, "\n") 89 | // re-activate 90 | page_claude.MustNavigate("https://claude.ai/api/account/statsig/" + org_uuid).MustWaitLoad() 91 | record_json := page_claude.MustNavigate("https://claude.ai/api/organizations/" + org_uuid + "/chat_conversations/" + new_uuid).MustElementX("//pre").MustText() 92 | record_chat_messages = gjson.Get(string(record_json), "chat_messages").String() 93 | record_count := gjson.Get(string(response_chat_messages), "#").Int() 94 | page_claude.MustNavigate("https://claude.ai/api/organizations/" + org_uuid).MustWaitLoad() 95 | time.Sleep(3 * time.Second) // delay to simulate human being 96 | //question = strings.Replace(question, `"`, `\"`, -1) // escape " in input text when code into json 97 | 98 | //d := `{"completion":{"prompt":"` + question + `","timezone":"Asia/Shanghai","model":"claude-2"},"organization_uuid":"` + org_uuid + `","conversation_uuid":"` + new_uuid + `","text":"` + question + `","attachments":[]}` 99 | d := `{"prompt":"` + question + `","timezone":"Asia/Bangkok","model":"claude-2.1","attachments":[],"files":[]}` 100 | //fmt.Println(d) 101 | //js := ` 102 | // (sdata, new_uuid) => { 103 | // var xhr = new XMLHttpRequest(); 104 | // xhr.open("POST", "https://claude.ai/api/append_message"); 105 | // xhr.setRequestHeader('Content-Type', 'application/json'); 106 | // xhr.setRequestHeader('Referer', 'https://claude.ai/chat/new_uuid'); 107 | // xhr.setRequestHeader('Origin', 'https://claude.ai'); 108 | // xhr.setRequestHeader('TE', 'trailers'); 109 | // xhr.setRequestHeader('Connection', 'keep-alive'); 110 | // xhr.setRequestHeader('Accept', 'text/event-stream, text/event-stream'); 111 | // xhr.onreadystatechange = function() { 112 | // if (xhr.readyState == XMLHttpRequest.DONE) { 113 | // var res_text = xhr.responseText; 114 | // console.log(res_text); 115 | // } 116 | // } 117 | // console.log(sdata); 118 | // xhr.send(sdata); 119 | // } 120 | // ` 121 | js := ` 122 | (sdata, org_uuid, new_uuid) => { 123 | var xhr = new XMLHttpRequest(); 124 | xhr.open("POST", "https://claude.ai/api/organizations/org_uuid/chat_conversations/new_uuid/completion"); 125 | xhr.setRequestHeader('Content-Type', 'application/json'); 126 | xhr.setRequestHeader('Referer', 'https://claude.ai/chat/new_uuid'); 127 | xhr.setRequestHeader('Origin', 'https://claude.ai'); 128 | xhr.setRequestHeader('TE', 'trailers'); 129 | xhr.setRequestHeader('Connection', 'keep-alive'); 130 | xhr.setRequestHeader('Accept', 'text/event-stream, text/event-stream'); 131 | xhr.onreadystatechange = function() { 132 | if (xhr.readyState == XMLHttpRequest.DONE) { 133 | var res_text = xhr.responseText; 134 | console.log(res_text); 135 | } 136 | } 137 | console.log(sdata); 138 | xhr.send(sdata); 139 | } 140 | ` 141 | page_claude.MustEval(js, d, org_uuid, new_uuid).Str() 142 | fmt.Println("Claude generating...", role) 143 | if role == ".all" { 144 | channel_claude <- "click_claude" 145 | } 146 | time.Sleep(3 * time.Second) // delay to simulate human being 147 | 148 | // wait answer 149 | var claude_response = false 150 | var response_json string 151 | for i := 1; i <= 20; i++ { 152 | if page_claude.MustNavigate("https://claude.ai/api/organizations/" + org_uuid + "/chat_conversations/" + new_uuid).MustHasX("//pre") { 153 | response_json = page_claude.MustNavigate("https://claude.ai/api/organizations/" + org_uuid + "/chat_conversations/" + new_uuid).MustElementX("//pre").MustText() 154 | response_chat_messages = gjson.Get(string(response_json), "chat_messages").String() 155 | count := gjson.Get(string(response_chat_messages), "#").Int() 156 | 157 | if response_chat_messages != record_chat_messages && count == record_count+2 { 158 | claude_response = true 159 | record_chat_messages = response_chat_messages 160 | break 161 | } 162 | } 163 | time.Sleep(3 * time.Second) 164 | } 165 | if claude_response == true { 166 | count := gjson.Get(string(response_chat_messages), "#").Int() 167 | answer := gjson.Get(string(response_json), "chat_messages.#(index=="+strconv.FormatInt(count-1, 10)+").text").String() 168 | channel_claude <- answer 169 | } else { 170 | channel_claude <- "✘✘ Claude, Please check the internet connection and verify login status." 171 | relogin_claude = true 172 | //page_claude.MustPDF("./tmp/Claude✘.pdf") 173 | } 174 | } 175 | } 176 | } 177 | 178 | } 179 | -------------------------------------------------------------------------------- /else/README.md-: -------------------------------------------------------------------------------- 1 | # Aih: Talk with Bard/ChatGPT/Claude2/Llama2 in the terminal. 2 | 3 | ![screenshot1](aih.gif) 4 | 5 | ## Usage 6 | Download [binary file](https://github.com/Databingo/aih/releases) then type: 7 | ```bash 8 | ./aih 9 | ``` 10 | ## Command list 11 | | Command | Operation| 12 | |------------|----------| 13 | |. | Select AI mode of Bard/ChatGPT/Claude/HuggingChat| 14 | |.proxy | Set proxy, for example: socks5://127.0.0.1:7890| 15 | |<< | Start multiple lines input mode| 16 | |>> | End multiple lines input mode| 17 | |↑ | Previous input| 18 | |↓ | Next input| 19 | |.c or .clear| Clear the screen| 20 | |.h or .history | Show history of conversations| 21 | |j | Scroll down| 22 | |k | Scroll up| 23 | |g | Scroll to top| 24 | |G | Scroll to bottom| 25 | |q or Enter | Back to conversation| 26 | |.help | Show help| 27 | |.exit | Exit Aih| 28 | 29 | ## Prerequisites 30 | - [Chrome Browser](https://google.com/chrome) 31 | - Free account of [Bard](https://bard.google.com), [Claude](https://claude.ai), [OpenAI](https://chat.openai.com), [HuggingChat](https://huggingface.co/chat) logged-in manually on your Chrome browser. 32 | - (Optional) Paid ChatGPT API on [Billing](https://platform.openai.com/account/billing/overview). 33 | 34 | ## Tips 35 | - Answer will be auotmatically saved in system clipboard for pasting. 36 | - All conversations were persisted in `history.txt` beside Aih binary. 37 | - More operations of command line at [Liner](https://github.com/peterh/liner#Line-editing). 38 | - All-In-One mode will display answers from all the AI modes. 39 | screenshot2 40 | 41 | ## Supported Operating Systems: 42 | - Mac 43 | - Linux 44 | - Windows 45 | 46 | ## Installation 47 | - Bash 48 | ``` 49 | $ git clone https://github.com/Databingo/aih 50 | $ go clean -cache && go clean -modcache 51 | $ cd aih && go mod tidy && go build 52 | ``` 53 | ## About Suggestions 54 | This is an open plan based on the idea of "Co-relation's enhancement of AI and human beings". If you have any suggestions, please write them in the Issues section. 55 | 56 | ## Acknowledgements 57 | - github.com/go-rod/rod 58 | - github.com/sashabaranov/go-openai 59 | -------------------------------------------------------------------------------- /else/bard.py: -------------------------------------------------------------------------------- 1 | import undetected_chromedriver as uc 2 | #from selenium import webdriver as uc 3 | import random,time,os,sys 4 | from selenium.webdriver.common.keys import Keys 5 | from selenium.webdriver.common.by import By 6 | from selenium.webdriver.support.ui import WebDriverWait 7 | from selenium.webdriver.support import expected_conditions as EC 8 | import json 9 | import sys 10 | 11 | # Restart session 12 | ######################### 13 | #driver = uc.Chrome(options=chrome_options, headless=True) 14 | chrome_options = uc.ChromeOptions() 15 | chrome_options.add_argument("--disable-extensions") 16 | chrome_options.add_argument("--disable-popup-blocking") 17 | chrome_options.add_argument("--profile-directory=Default") 18 | chrome_options.add_argument("--ignore-certificate-errors") 19 | chrome_options.add_argument("--disable-plugins-discovery") 20 | chrome_options.add_argument("--incognito") 21 | chrome_options.add_argument("--headless") 22 | chrome_options.add_argument("user_agent=DN") 23 | driver = uc.Chrome(options=chrome_options) 24 | 25 | # Load cookie 26 | driver.get("https://bard.google.com") 27 | with open("./2.json", "r", newline='') as inputdata: 28 | ck = json.load(inputdata) 29 | for c in ck: 30 | driver.add_cookie({k:c[k] for k in {'name', 'value'}}) 31 | 32 | # Renew with cookie 33 | driver.get("https://bard.google.com") 34 | wait = WebDriverWait(driver, 20) 35 | try: 36 | work = wait.until(EC.visibility_of_element_located((By.XPATH, "//textarea[@id='mat-input-0']"))) 37 | print("login work") 38 | except: 39 | print("relogin") 40 | #open("./2.json", "w").close() 41 | driver.quit() 42 | os.exit() 43 | 44 | wait = WebDriverWait(driver, 30000) 45 | while 1: 46 | ori = input(":") 47 | if ori: 48 | #for line in sys.stdin: 49 | # message = line.strip() 50 | # ori = message.replace("(-:]", " ") 51 | work.send_keys(ori) 52 | driver.find_element(By.XPATH, "//button[@mattooltip='Submit']").click() 53 | #ini_source = driver.page_source 54 | if ori: 55 | try: 56 | img_thinking = wait.until(EC.presence_of_element_located((By.XPATH, "//img[contains(@src, 'https://www.gstatic.com/lamda/images/sparkle_thinking_v2_e272afd4f8d4bbd25efe.gif')]"))) 57 | #print("get img_thinking") 58 | img = wait.until(EC.presence_of_element_located((By.XPATH, "//img[contains(@src, 'https://www.gstatic.com/lamda/images/sparkle_resting_v2_1ff6f6a71f2d298b1a31.gif')]"))) 59 | #print("get img") 60 | response = img.find_element(By.XPATH, "ancestor::model-response") 61 | #print("get response content img") 62 | google = response.find_element(By.XPATH, ".//button[@aria-label='Google it']") 63 | 64 | contents = response.find_elements(By.XPATH, ".//message-content") 65 | texts= "\n".join(content.text for content in contents) 66 | text = "(-:]".join(line for line in texts.splitlines() if line) 67 | 68 | text = response.text 69 | text = text.replace("\n","(-:]") 70 | text = text.replace("View other drafts","") 71 | text = text.replace("Regenerate draft","") 72 | text = text.replace("thumb_up","") 73 | text = text.replace("thumb_down","") 74 | text = text.replace("upload","") 75 | text = text.replace("Google it","") 76 | text = text.replace("more_vert","") 77 | text = text.replace("volume_up","") 78 | text = "(-:]".join(line for line in text.splitlines() if line) 79 | print(text) 80 | sys.stdout.flush() 81 | 82 | cookies = driver.get_cookies() 83 | with open("./2.json", "w", newline='') as outputdata: 84 | json.dump(cookies, outputdata) 85 | 86 | except Exception as e: 87 | pass 88 | 89 | -------------------------------------------------------------------------------- /else/chatgpt.py: -------------------------------------------------------------------------------- 1 | import undetected_chromedriver as uc 2 | import random,time,os,sys 3 | from selenium.webdriver.common.keys import Keys 4 | from selenium.webdriver.common.by import By 5 | from selenium.webdriver.support.ui import WebDriverWait 6 | from selenium.webdriver.support import expected_conditions as EC 7 | import json 8 | import sys 9 | 10 | # Restart session 11 | ######################### 12 | #driver = uc.Chrome(options=chrome_options, headless=True) 13 | chrome_options = uc.ChromeOptions() 14 | chrome_options.add_argument("--disable-extensions") 15 | chrome_options.add_argument("--disable-popup-blocking") 16 | chrome_options.add_argument("--profile-directory=Default") 17 | chrome_options.add_argument("--ignore-certificate-errors") 18 | chrome_options.add_argument("--disable-plugins-discovery") 19 | chrome_options.add_argument("--incognito") 20 | chrome_options.add_argument("--headless") 21 | chrome_options.add_argument("user_agent=DN") 22 | driver = uc.Chrome(options=chrome_options) 23 | 24 | # Load cookie 25 | driver.get("https://chat.openai.com") 26 | with open("./4.json", "r", newline='') as inputdata: 27 | ck = json.load(inputdata) 28 | for c in ck: 29 | driver.add_cookie({k:c[k] for k in {'name', 'value'}}) 30 | 31 | # Renew with cookie 32 | driver.get("https://chat.openai.com") 33 | wait = WebDriverWait(driver, 200) 34 | try: 35 | notice1 = wait.until(EC.visibility_of_element_located((By.XPATH, "//h4[contains(text(), 'This is a free research preview.')]"))) 36 | #print("notice1") 37 | next1 = wait.until(EC.visibility_of_element_located((By.XPATH, "//button/div[contains(text(), 'Next')]"))) 38 | #print("next1") 39 | next1.click() 40 | #print("next1.click") 41 | notice2 = wait.until(EC.visibility_of_element_located((By.XPATH, "//h4[contains(text(), 'How we collect data')]"))) 42 | #print("notice2") 43 | next2 = wait.until(EC.visibility_of_element_located((By.XPATH, "//button/div[contains(text(), 'Next')]"))) 44 | #print("next2") 45 | next2.click() 46 | #print("next2.click") 47 | notice3 = wait.until(EC.visibility_of_element_located((By.XPATH, "//h4[contains(text(), 'love your feedback!')]"))) 48 | #print("notice3") 49 | next3 = wait.until(EC.visibility_of_element_located((By.XPATH, "//button/div[contains(text(), 'Done')]"))) 50 | #print("next3") 51 | next3.click() 52 | #print("next3.click") 53 | driver.find_element(By.XPATH, "//a[contains(text(), 'New chat')]").click() 54 | input_space = wait.until(EC.visibility_of_element_located((By.XPATH, "//textarea[@id='prompt-textarea']"))) 55 | #print("login work") 56 | except: 57 | print("relogin") 58 | #open("./2.json", "w").close() 59 | driver.quit() 60 | os.exit() 61 | 62 | while 1: 63 | ori = input(":") 64 | if ori: 65 | #for line in sys.stdin: 66 | # message = line.strip() 67 | # ori = message.replace("(-:]", " ") 68 | input_space.send_keys(ori) 69 | driver.find_element(By.XPATH, "//button//svg:path[@d='M.5 1.163A1 1 0 0 1 1.97.28l12.868 6.837a1 1 0 0 1 0 1.766L1.969 15.72A1 1 0 0 1 .5 14.836V10.33a1 1 0 0 1 .816-.983L8.5 8 1.316 6.653A1 1 0 0 1 .5 5.67V1.163Z']").click() 70 | #ini_source = driver.page_source 71 | if ori: 72 | try: 73 | retry_icon = wait.until(EC.presence_of_element_located((By.XPATH, "//svg:path[@d='M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15']"))) 74 | #print("get retry_icon") 75 | content = retry_icon.find_element(By.XPATH, "(//div[contains(@class, 'group w-full')])[last()]") 76 | text = content.get_attribute("textContent") 77 | text = text.replace("ChatGPTChatGPT","") 78 | text = text.replace("1 / 1","") 79 | text = text.replace("\n","(-:]") 80 | print(text) 81 | sys.stdout.flush() 82 | cookies = driver.get_cookies() 83 | with open("./4.json", "w", newline='') as outputdata: 84 | json.dump(cookies, outputdata) 85 | 86 | except Exception as e: 87 | pass 88 | 89 | 90 | -------------------------------------------------------------------------------- /else/claude2.py: -------------------------------------------------------------------------------- 1 | import undetected_chromedriver as uc 2 | import random,time,os,sys 3 | from selenium.webdriver.common.keys import Keys 4 | from selenium.webdriver.common.by import By 5 | from selenium.webdriver.support.ui import WebDriverWait 6 | from selenium.webdriver.support import expected_conditions as EC 7 | import json 8 | import sys 9 | 10 | # Restart session 11 | ######################### 12 | #driver = uc.Chrome(options=chrome_options, headless=True) 13 | chrome_options = uc.ChromeOptions() 14 | chrome_options.add_argument("--disable-extensions") 15 | chrome_options.add_argument("--disable-popup-blocking") 16 | chrome_options.add_argument("--profile-directory=Default") 17 | chrome_options.add_argument("--ignore-certificate-errors") 18 | chrome_options.add_argument("--disable-plugins-discovery") 19 | chrome_options.add_argument("--incognito") 20 | #chrome_options.add_argument("--headless") 21 | chrome_options.add_argument("user_agent=DN") 22 | driver = uc.Chrome(options=chrome_options) 23 | 24 | driver.get("https://claude.ai") 25 | 26 | # Load cookie 27 | with open("./3.json", "r", newline='') as inputdata: 28 | ck = json.load(inputdata) 29 | for c in ck: 30 | driver.add_cookie({k:c[k] for k in {'name', 'value'}}) 31 | 32 | # Renew with cookie 33 | driver.get("https://claude.ai") 34 | wait = WebDriverWait(driver, 200) 35 | try: 36 | work = wait.until(EC.visibility_of_element_located((By.XPATH, "//p[@data-placeholder='Message Claude or search past chats...']"))) 37 | #driver.find_element(By.XPATH, "//button[@class='sc-dAOort']").click() 38 | driver.find_element(By.XPATH, "//div[contains(text(), 'Start a new chat')]").click() 39 | input_space = wait.until(EC.visibility_of_element_located((By.XPATH, "//p[@data-placeholder='Message Claude...']"))) 40 | print("login work") 41 | except: 42 | print("relogin") 43 | #open("./3.json", "w").close() 44 | driver.quit() 45 | os.exit() 46 | 47 | 48 | while 1: 49 | ori = input(":") 50 | if ori: 51 | #for line in sys.stdin: 52 | # message = line.strip() 53 | # ori = message.replace("(-:]", " ") 54 | input_space.send_keys(ori) 55 | driver.find_element(By.XPATH, "//button[@aria-label='Send Message']").click() 56 | if ori: 57 | try: 58 | retry_icon = wait.until(EC.presence_of_element_located((By.XPATH, "//svg:path[@d= 'M224,128a96,96,0,0,1-94.71,96H128A95.38,95.38,0,0,1,62.1,197.8a8,8,0,0,1,11-11.63A80,80,0,1,0,71.43,71.39a3.07,3.07,0,0,1-.26.25L44.59,96H72a8,8,0,0,1,0,16H24a8,8,0,0,1-8-8V56a8,8,0,0,1,16,0V85.8L60.25,60A96,96,0,0,1,224,128Z']"))) 59 | #print("get last retry_icon") 60 | content = retry_icon.find_element(By.XPATH, "preceding::div[2]") 61 | text = content.get_attribute("textContent") 62 | text = text.replace("\n","(-:]") 63 | print(text) 64 | sys.stdout.flush() 65 | 66 | # Save cookie 67 | cookies = driver.get_cookies() 68 | with open("./3.json", "w", newline='') as outputdata: 69 | json.dump(cookies, outputdata) 70 | 71 | except Exception as e: 72 | pass 73 | 74 | 75 | -------------------------------------------------------------------------------- /else/eng/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Databingo/aih/eng 2 | 3 | go 1.19 4 | 5 | require github.com/chromedp/chromedp v0.9.1 6 | 7 | require ( 8 | github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 // indirect 9 | github.com/chromedp/sysutil v1.0.0 // indirect 10 | github.com/gobwas/httphead v0.1.0 // indirect 11 | github.com/gobwas/pool v0.2.1 // indirect 12 | github.com/gobwas/ws v1.1.0 // indirect 13 | github.com/josharian/intern v1.0.0 // indirect 14 | github.com/mailru/easyjson v0.7.7 // indirect 15 | golang.org/x/sys v0.6.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /else/eng/go.sum: -------------------------------------------------------------------------------- 1 | github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9 h1:wMSvdj3BswqfQOXp2R1bJOAE7xIQLt2dlMQDMf836VY= 2 | github.com/chromedp/cdproto v0.0.0-20230220211738-2b1ec77315c9/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= 3 | github.com/chromedp/chromedp v0.9.1 h1:CC7cC5p1BeLiiS2gfNNPwp3OaUxtRMBjfiw3E3k6dFA= 4 | github.com/chromedp/chromedp v0.9.1/go.mod h1:DUgZWRvYoEfgi66CgZ/9Yv+psgi+Sksy5DTScENWjaQ= 5 | github.com/chromedp/sysutil v1.0.0 h1:+ZxhTpfpZlmchB58ih/LBHX52ky7w2VhQVKQMucy3Ic= 6 | github.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww= 7 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 8 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 9 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 10 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 11 | github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA= 12 | github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0= 13 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 14 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 15 | github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= 16 | github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= 17 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 18 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 19 | github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= 20 | github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= 21 | golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 22 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 23 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | -------------------------------------------------------------------------------- /else/eng/playphrase.go: -------------------------------------------------------------------------------- 1 | package eng 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/chromedp/cdproto/cdp" 7 | "github.com/chromedp/cdproto/input" 8 | "github.com/chromedp/chromedp" 9 | "log" 10 | "strings" 11 | "time" 12 | ) 13 | 14 | func Play(words []string) { 15 | ctx, cancel := chromedp.NewContext(context.Background()) 16 | defer cancel() 17 | 18 | // Set up Chrome options 19 | opts := append(chromedp.DefaultExecAllocatorOptions[:], 20 | chromedp.Flag("headless", false), 21 | // chromedp.Flag("disable-gpu", true), 22 | // chromedp.Flag("no-sandbox", true), 23 | // chromedp.Flag("disable-infobars", true), 24 | // chromedp.Flag("disable-extensions", true), 25 | // chromedp.Flag("disable-web-security", true), 26 | // chromedp.Flag("mute-audio", true), 27 | chromedp.Flag("mute-audio", false), 28 | ) 29 | allocCtx, cancel := chromedp.NewExecAllocator(ctx, opts...) 30 | defer cancel() 31 | 32 | // Create a WebDriver object for Chrome browser 33 | ctx, cancel = chromedp.NewContext(allocCtx) 34 | defer cancel() 35 | 36 | // Maximize the window for keep the stream alive 37 | // This function has been removed as it returns an error, and it is not being used anywhere else 38 | // if err := chromedp.Run(ctx, chromedp.MaximizeWindow()); err != nil { 39 | // log.Fatal(err) 40 | // } 41 | 42 | if err := chromedp.Run(ctx, chromedp.Navigate("https://playphrase.me/")); err != nil { 43 | fmt.Println(err) 44 | } 45 | // Wait for Play icon 46 | if err := chromedp.Run(ctx, chromedp.WaitVisible(`//i[@class="material-icons-outlined"]`, chromedp.BySearch)); err != nil { 47 | fmt.Println(err) 48 | } 49 | if err := chromedp.Run(ctx, chromedp.Click("//body", chromedp.BySearch)); err != nil { 50 | fmt.Println(err) 51 | } 52 | 53 | search_bar := "#search-input" 54 | 55 | for _, phrase := range words { 56 | 57 | for { 58 | var value string 59 | if err := chromedp.Run(ctx, chromedp.Value(search_bar, &value)); err != nil { 60 | log.Fatal(err) 61 | } 62 | if value == phrase { 63 | break 64 | } 65 | if err := chromedp.Run(ctx, chromedp.Click(`//i[contains(text(),"close")]`, chromedp.BySearch)); err != nil { 66 | log.Fatal(err) 67 | } 68 | // SendKeys not work well with wrong characters. 69 | // if err := chromedp.Run(ctx, chromedp.SendKeys(search_bar, phrase)); err != nil { 70 | // log.Fatal(err) 71 | // } 72 | var nodes []*cdp.Node 73 | _ = chromedp.Run(ctx, chromedp.Nodes("#search-input", &nodes, chromedp.ByQuery)) 74 | _ = chromedp.Run(ctx, chromedp.MouseClickNode(nodes[0])) 75 | _ = chromedp.Run(ctx, input.InsertText(phrase)) 76 | 77 | } 78 | 79 | // Check if the search result count is "1/0" 80 | if err := chromedp.Run(ctx, chromedp.Sleep(5*time.Second)); err != nil { 81 | log.Fatal(err) 82 | } 83 | var search_result_count string 84 | if err := chromedp.Run(ctx, chromedp.Text("li div.search-result-count", &search_result_count)); err != nil { 85 | log.Fatal(err) 86 | } 87 | if search_result_count == "1/0" { 88 | //fmt.Println("Find nothing. Next!") 89 | continue 90 | } 91 | 92 | ch := make(chan bool) 93 | go func() { 94 | // Check if the page source contains the message "If you are not a sponsor you have a limit on our site." 95 | for { 96 | var content string 97 | if err := chromedp.Run(ctx, chromedp.InnerHTML("body", &content)); err != nil { 98 | log.Fatal(err) 99 | } 100 | if strings.Contains(content, "If you are not a sponsor you have a limit on our site.") { 101 | //fmt.Println("Played 5 already. Next!") 102 | close(ch) 103 | break 104 | } 105 | } 106 | 107 | }() 108 | 109 | <-ch 110 | 111 | } 112 | 113 | // Close the Chrome browser 114 | // Shutdown() function is not present in chromedp.Context. Using Stop() function instead. 115 | if err := chromedp.Run(ctx, chromedp.Stop()); err != nil { 116 | log.Fatal(err) 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /else/huggingchat.py: -------------------------------------------------------------------------------- 1 | import undetected_chromedriver as uc 2 | import random,time,os,sys 3 | from selenium.webdriver.common.keys import Keys 4 | from selenium.webdriver.common.by import By 5 | from selenium.webdriver.support.ui import WebDriverWait 6 | from selenium.webdriver.support import expected_conditions as EC 7 | import json 8 | import sys 9 | 10 | ######################### 11 | chrome_options = uc.ChromeOptions() 12 | chrome_options.add_argument("--disable-extensions") 13 | chrome_options.add_argument("--disable-popup-blocking") 14 | chrome_options.add_argument("--profile-directory=Default") 15 | chrome_options.add_argument("--ignore-certificate-errors") 16 | chrome_options.add_argument("--disable-plugins-discovery") 17 | chrome_options.add_argument("--incognito") 18 | #chrome_options.add_argument("--headless") 19 | #chrome_options.add_argument("user_agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/604.1 Edg/115.0.100.0") 20 | chrome_options.add_argument("user_agent=DN") 21 | driver = uc.Chrome(options=chrome_options) 22 | 23 | driver.get("https://huggingface.co/chat") 24 | with open("./5.json", "r", newline='') as inputdata: 25 | ck = json.load(inputdata) 26 | for c in ck: 27 | driver.add_cookie({k:c[k] for k in {'name', 'value'}}) 28 | 29 | # Renew with cookie 30 | driver.get("https://huggingface.co/chat") 31 | wait = WebDriverWait(driver, 200) 32 | try: 33 | input_space = wait.until(EC.visibility_of_element_located((By.XPATH, "//textarea[@enterkeyhint='send']"))) 34 | print("login work") 35 | except: 36 | print("relogin") 37 | driver.quit() 38 | os.exit() 39 | 40 | while 1: 41 | ori = input(":") 42 | if ori: 43 | #for line in sys.stdin: 44 | # message = line.strip() 45 | # ori = message.replace("(-:]", " ") 46 | input_space = wait.until(EC.visibility_of_element_located((By.XPATH, "//textarea[@enterkeyhint='send']"))) 47 | input_space.send_keys(ori) 48 | driver.find_element(By.XPATH, "//button//svg:path[@d='M27.71 4.29a1 1 0 0 0-1.05-.23l-22 8a1 1 0 0 0 0 1.87l8.59 3.43L19.59 11L21 12.41l-6.37 6.37l3.44 8.59A1 1 0 0 0 19 28a1 1 0 0 0 .92-.66l8-22a1 1 0 0 0-.21-1.05Z']").click() 49 | if ori: 50 | try: 51 | stop_icon = wait.until(EC.presence_of_element_located((By.XPATH, "//svg:path[@d='M24 6H8a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2Z']"))) 52 | #print("get stop_icon") 53 | wait.until(EC.staleness_of(stop_icon)) 54 | #print("disappear stop_icon") 55 | img = driver.find_element(By.XPATH, "(//img[contains(@src, 'https://huggingface.co/avatars/2edb18bd0206c16b433841a47f53fa8e.svg')])[last()]") 56 | #print("img") 57 | content = img.find_element(By.XPATH, "following-sibling::div[1]") 58 | text = content.get_attribute("textContent") 59 | text = text.replace("\n","(-:]") 60 | print(text) 61 | sys.stdout.flush() 62 | cookies = driver.get_cookies() 63 | with open("./5.json", "w", newline='') as outputdata: 64 | json.dump(cookies, outputdata) 65 | 66 | except Exception as e: 67 | pass 68 | 69 | 70 | -------------------------------------------------------------------------------- /else/thought.txt: -------------------------------------------------------------------------------- 1 | 2 | English: AI Research Artifact 3 | Japanese: AI 研究の神器 4 | Korean: AI 연구 슈퍼 도구 5 | French: L'artefact de recherche AI 6 | Spanish: El artefacto de investigación de IA 7 | German: Das AI-Forschungsartefakt 8 | Russian: Артефакт AI-исследования 9 | Portuguese: Artefato de Pesquisa de IA 10 | Italian: Artefatto di ricerca AI 11 | Hindi: एआई शोध का सुपर टूल 12 | Arabic: قطعة أثرية لبحث الذكاء الاصطناعي 13 | Vietnamese: Siêu công cụ nghiên cứu AI 14 | Thai: เครื่องมือวิจัย AI ยอดเยี่ยม 15 | Indonesian: Super alat penelitian AI 16 | Malay: Super alat penyelidikan AI 17 | Finnish: Supertyökalu tekoälyn tutkimukseen 18 | Swedish: Superverktyg för AI-forskning 19 | Dutch: Superhulpmiddel voor AI-onderzoek 20 | Romanian: Super unealtă de cercetare în domeniul inteligenței artificiale 21 | Greek: Υπερ εργαλείο έρευνας τεχνητής νοημοσύνης 22 | 23 | Indonesian: 24 | Portuguese: 25 | Dutch: 26 | Polish: 27 | Hungarian: 28 | Zulu: 29 | 30 | | 研究の神器 31 | | 연구 슈퍼 도구 32 | | L'artefact de recherche 33 | | El artefacto de investigación de 34 | | Das Forschungsartefakt 35 | | Артефакт исследования 36 | | Artefato de Pesquisa de 37 | | Artefatto di ricerca 38 | | एआई शोध का सुपर टूल 39 | | قطعة أثرية لبحث الذكاء الاصطناعي 40 | | เครื่องมือวิจัย ยอดเยี่ยม 41 | | Supertyökalu tekoälyn tutkimukseen 42 | | Superverktyg för forskning 43 | | Superhulpmiddel voor onderzoek 44 | | Υπερ εργαλείο έρευνας τεχνητής νοημοσύνης 45 | 46 | | 科研神器 | 연구 슈퍼 도구 | L'artefact de recherche | El artefacto de investigación de | Das Forschungsartefakt | Артефакт исследования | Artefato de Pesquisa de | Artefatto di ricerca | एआई शोध का सुपर टूल | قطعة أثرية لبحث الذكاء الاصطناعي | เครื่องมือวิจัย ยอดเยี่ยม | Google | API | CLI | VIM | 47 | 48 | 49 | 50 | https://huggingface.co/chat/__data.json?x-sveltekit-invalidated=1_ conversation list 51 | https://huggingface.co/chat/conversation/64f30169c498fd14c9f41469 404|could DELETE 52 | https://huggingface.co/chat/conversation/64f30169c498fd14c9f41469/web-search? 53 | 54 | 55 | TODO: 56 | yaml for histoy 57 | //headless false to get cookie? NO 58 | check chatGPT verify mask? Difficult 59 | //color setting of vi for symbols (now is black can't see in black background) 60 | //restart for faulse AI model? 61 | //escapt %v out of %!s(MISSING): on webpage Done 62 | // add Falcon Done 63 | 1.one file for all notes 64 | 2.one file for all pass 65 | //3.hasX stop then hasX submit 66 | // Hard to insert check 4.internet error of llama2/falcon180 67 | //Done 5.merger proxies-groups 68 | //6.optimise yaml model in merger 69 | //7.sell books 70 | //8.select cloth/shoes to sell 71 | //9.prepare gift to guards wine? 72 | 10.prompt engineering/sell? deeper? 73 | 11.aih prompt select/create/save function 74 | 12.group for prompt engineering? 75 | //13.tester 30 parameter of cli 76 | 15.aih .i instruction list to vi mode? 77 | 78 | ----------- universal_primer --------- 79 | I want you, ChatGPT, to select the most suitable and knowledgeable person or expert/team for the question I am going to ask you. 80 | This role/team possesses extensive knowledge and expertise in the subject matter, with access to a comprehensive knowledge graph. 81 | They also provide both a detailed and a big-picture understanding of the subject. 82 | You will adhere to the role or team as long as necessary and choose another if it goes off-topic. 83 | 84 | Commands: 85 | "/og" --> utilize the previously selected role to answer the query. 86 | "/re" --> reevaluate your last answer and if necessary correct yourself in a numbered summary. 87 | 88 | Localisation: CH-EN 89 | Units: Metric 90 | 91 | Example 1: 92 | OP: I want to analyse GPS Tracking data in a Flask Application. 93 | ChatGPT: Analyzing Topics and Roles... 94 | Topic 1: Analysing GPS Tracking Data, role: Data Analyst + GIS-Specialist 95 | Topic 2: Flask Application Development, role: Backend Developer + Frontend Developer + UX Designer 96 | [Team lead after collecting feedback from roles]: Sure, my Team has a few open Questions, let's begin: 97 | 98 | 1. What do you want to accomplish? 99 | X. (other unclarified points)] 100 | 101 | Abstract: 102 | 1. Create topic list based on query. 103 | 2. Select role/Team based on Topics. 104 | 3. Answer query from the role/Team perspective, clarify missing information if necessary. 105 | 106 | Task Summary: You will select an overqualified role or team with a team lead based on the question, which will provide the highest expertise, and then answer the question from the perspective of that role or team lead. 107 | If some key details or other 'could be relevant' details are not defined, please ask for clarifications (measurements, quantities, how much). 108 | You are not assuming anything, instead [you|the team] will seek out the missing information. 109 | You are fluent in all languages. 110 | Provide concise, straight-to-the-point responses. 111 | Use step-by-step thinking. 112 | Adopt fitting personalities (e.g., if you are a Data Analyst, be more precise and structured; if you are a UX Designer, be more creative and open-minded). 113 | You must select a role/Team, and will inform other roles/Teams if you need help answering the user queries. 114 | 115 | Begin each thread with "Analyzing Topic... (…), Selected [role|TEAM with TEAMLEAD]", then proceed to answer as [role|TEAMLEAD]: 116 | 117 | Confirm instructions with an short and precise "Ok". 118 | -------------------------------------------------------------------------------- /falcon180.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "github.com/go-rod/rod" 7 | "github.com/go-rod/stealth" 8 | "time" 9 | ) 10 | 11 | // Set up client of falcon180-180B (Rod version) 12 | var page_falcon180 *rod.Page 13 | var relogin_falcon180 = true 14 | var channel_falcon180 chan string 15 | 16 | func Falcon180() { 17 | channel_falcon180 = make(chan string) 18 | defer func() { 19 | if err := recover(); err != nil { 20 | relogin_falcon180 = true 21 | } 22 | }() 23 | //page_hc = browser.MustPage("https://huggingface.co/chat") 24 | page_falcon180 = stealth.MustPage(browser) 25 | //page_falcon180.MustNavigate("https://huggingface.co/spaces/tiiuae/falcon-180b-demo") 26 | page_falcon180.MustNavigate("https://tiiuae-falcon-180b-demo.hf.space/?__theme=light") 27 | for i := 1; i <= 30; i++ { 28 | if page_falcon180.MustHasX("//textarea[@data-testid='textbox']") { 29 | relogin_falcon180 = false 30 | break 31 | } 32 | time.Sleep(time.Second) 33 | } 34 | if relogin_falcon180 == true { 35 | sprint("✘ Falcon180") 36 | //page_hc.MustPDF("./tmp/HuggingChat✘.pdf") 37 | } 38 | if relogin_falcon180 == false { 39 | sprint("✔ Falcon180") 40 | for { 41 | select { 42 | case question := <-channel_falcon180: 43 | //page_falcon180.MustActivate() 44 | //fmt.Println("Falcon180 received question...", question) 45 | for i := 1; i <= 20; i++ { 46 | if page_falcon180.MustHasX("//textarea[@data-testid='textbox']") { 47 | page_falcon180.MustElementX("//textarea[@data-testid='textbox']").MustInput(question) 48 | break 49 | } 50 | time.Sleep(time.Second) 51 | } 52 | //fmt.Println("Falcon180 input typed...") 53 | for i := 1; i <= 20; i++ { 54 | //if page_falcon180.MustHasX("//button[contains(text(), 'Submit')]") { 55 | if page_falcon180.MustHasX("//button[@id='component-17']") { 56 | page_falcon180.MustElementX("//button[@id='component-17']").MustClick() 57 | break 58 | } 59 | time.Sleep(time.Second) 60 | } 61 | fmt.Println("Falcon180 generating...") 62 | //page_falcon180.MustActivate() // Sometime three dot to hang 63 | //if role == ".all" { 64 | // channel_falcon180 <- "click_falcon180" 65 | //} 66 | //// Check Error 67 | //channel_falcon180_check := make(chan string) 68 | //go func() { 69 | // for i := 1; i <= 20; i++ { 70 | // if page_falcon180.MustHasX("//*[contains(text(), 'Too much traffic, please try again')]") { 71 | // channel_falcon180 <- "✘✘ Falcon180, Please check the internet connection and verify login status. Traffic." 72 | // fmt.Println("Falcon180 too much traffic...") 73 | // relogin_falcon180 = true 74 | // close(channel_falcon180_check) 75 | // break 76 | // } 77 | // time.Sleep(1 * time.Second) 78 | // } 79 | //}() 80 | 81 | // stop_icon 82 | var stop_icon_disappear = false 83 | err := rod.Try(func() { 84 | page_falcon180.Timeout(10 * time.Second).MustElementX("//button[@id='component-18']").MustWaitVisible().CancelTimeout() 85 | }) 86 | if err == nil { 87 | err = rod.Try(func() { 88 | page_falcon180.Timeout(80 * time.Second).MustElementX("//button[@id='component-18']").MustWaitInvisible().CancelTimeout() 89 | }) 90 | if err == nil { 91 | stop_icon_disappear = true 92 | } else { 93 | //fmt.Println("err::::", err) 94 | } 95 | } else { 96 | //fmt.Println("err::", err) 97 | } 98 | 99 | if stop_icon_disappear == true { 100 | answer := page_falcon180.MustElementX("(//div[@data-testid='bot'])[last()]").MustText() 101 | channel_falcon180 <- answer 102 | } else { 103 | channel_falcon180 <- "✘✘ Falcon180, Please check the internet connection and verify login status." 104 | relogin_falcon180 = true 105 | //page_hc.MustPDF("./tmp/HuggingChat✘.pdf") 106 | 107 | } 108 | } 109 | } 110 | } 111 | 112 | } 113 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module aih 2 | 3 | go 1.19 4 | 5 | require ( 6 | //github.com/Databingo/aih/ry v0.0.0-bef1172 7 | github.com/atotto/clipboard v0.1.4 8 | github.com/creack/pty v1.1.18 9 | // github.com/gdamore/tcell/v2 v2.6.1-0.20230827060410-08c7757cd1c9 10 | github.com/go-rod/rod v0.114.5 11 | github.com/go-rod/stealth v0.4.9 12 | github.com/google/go-github v17.0.0+incompatible 13 | github.com/google/uuid v1.3.1 14 | github.com/manifoldco/promptui v0.9.0 15 | github.com/peterh/liner v1.2.2 16 | github.com/rivo/tview v0.0.0-20230511053024-822bd067b165 17 | github.com/sashabaranov/go-openai v1.9.0 18 | github.com/tidwall/gjson v1.14.4 19 | github.com/tidwall/sjson v1.2.5 20 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 21 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 22 | jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 23 | ) 24 | 25 | // replace github.com/Databingo/aih/ry => ./ry 26 | 27 | require github.com/gdamore/tcell/v2 v2.6.0 28 | 29 | require ( 30 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e // indirect 31 | github.com/gdamore/encoding v1.0.0 // indirect 32 | github.com/golang/protobuf v1.5.3 // indirect 33 | github.com/google/go-cmp v0.5.7 // indirect 34 | github.com/google/go-querystring v1.1.0 // indirect 35 | github.com/lucasb-eyer/go-colorful v1.2.0 // indirect 36 | github.com/mattn/go-runewidth v0.0.14 // indirect 37 | github.com/olekukonko/tablewriter v0.0.5 // indirect 38 | github.com/rivo/uniseg v0.4.4 // indirect 39 | github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect 40 | github.com/tidwall/match v1.1.1 // indirect 41 | github.com/tidwall/pretty v1.2.0 // indirect 42 | github.com/ysmood/fetchup v0.2.3 // indirect 43 | github.com/ysmood/goob v0.4.0 // indirect 44 | github.com/ysmood/got v0.34.1 // indirect 45 | github.com/ysmood/gson v0.7.3 // indirect 46 | github.com/ysmood/leakless v0.8.0 // indirect 47 | golang.org/x/net v0.9.0 // indirect 48 | golang.org/x/sys v0.11.0 // indirect 49 | golang.org/x/term v0.9.0 // indirect 50 | golang.org/x/text v0.12.0 // indirect 51 | google.golang.org/appengine v1.6.6 // indirect 52 | google.golang.org/protobuf v1.30.0 // indirect 53 | ) 54 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= 3 | github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= 4 | github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= 5 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 6 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= 7 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 8 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= 9 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 10 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 11 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 12 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 13 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 14 | github.com/gdamore/tcell/v2 v2.6.0 h1:OKbluoP9VYmJwZwq/iLb4BxwKcwGthaa1YNBJIyCySg= 15 | github.com/gdamore/tcell/v2 v2.6.0/go.mod h1:be9omFATkdr0D9qewWW3d+MEvl5dha+Etb5y65J2H8Y= 16 | github.com/go-rod/rod v0.113.0/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw= 17 | github.com/go-rod/rod v0.114.5 h1:1x6oqnslwFVuXJbJifgxspJUd3O4ntaGhRLHt+4Er9c= 18 | github.com/go-rod/rod v0.114.5/go.mod h1:aiedSEFg5DwG/fnNbUOTPMTTWX3MRj6vIs/a684Mthw= 19 | github.com/go-rod/stealth v0.4.9 h1:X2PmQk4DUF2wzw6GOsWjW/glb8K5ebnftbEvLh7MlZ4= 20 | github.com/go-rod/stealth v0.4.9/go.mod h1:eAzyvw8c0iAd5nJJsSWeh0fQ5z94vCIfdi1hUmYDimc= 21 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 22 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 23 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 24 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 25 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 26 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 28 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 29 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 30 | github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= 31 | github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= 32 | github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= 33 | github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= 34 | github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= 35 | github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 36 | github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= 37 | github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 38 | github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA= 39 | github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg= 40 | github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 41 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 42 | github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= 43 | github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 44 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 45 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 46 | github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= 47 | github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= 48 | github.com/rivo/tview v0.0.0-20230511053024-822bd067b165 h1:YMycYmUdmLI7ZTn86HUEDM8E8fCMz7twtysBW3SlG0c= 49 | github.com/rivo/tview v0.0.0-20230511053024-822bd067b165/go.mod h1:nVwGv4MP47T0jvlk7KuTTjjuSmrGO4JF0iaiNt4bufE= 50 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 51 | github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 52 | github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= 53 | github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 54 | github.com/sashabaranov/go-openai v1.9.0 h1:NoiO++IISxxJ1pRc0n7uZvMGMake0G+FJ1XPwXtprsA= 55 | github.com/sashabaranov/go-openai v1.9.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 56 | github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= 57 | github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM= 58 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 59 | github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= 60 | github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 61 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 62 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 63 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 64 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 65 | github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= 66 | github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= 67 | github.com/ysmood/fetchup v0.2.3 h1:ulX+SonA0Vma5zUFXtv52Kzip/xe7aj4vqT5AJwQ+ZQ= 68 | github.com/ysmood/fetchup v0.2.3/go.mod h1:xhibcRKziSvol0H1/pj33dnKrYyI2ebIvz5cOOkYGns= 69 | github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= 70 | github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= 71 | github.com/ysmood/gop v0.0.2 h1:VuWweTmXK+zedLqYufJdh3PlxDNBOfFHjIZlPT2T5nw= 72 | github.com/ysmood/gop v0.0.2/go.mod h1:rr5z2z27oGEbyB787hpEcx4ab8cCiPnKxn0SUHt6xzk= 73 | github.com/ysmood/got v0.34.1 h1:IrV2uWLs45VXNvZqhJ6g2nIhY+pgIG1CUoOcqfXFl1s= 74 | github.com/ysmood/got v0.34.1/go.mod h1:yddyjq/PmAf08RMLSwDjPyCvHvYed+WjHnQxpH851LM= 75 | github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= 76 | github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= 77 | github.com/ysmood/gson v0.7.3 h1:QFkWbTH8MxyUTKPkVWAENJhxqdBa4lYTQWqZCiLG6kE= 78 | github.com/ysmood/gson v0.7.3/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= 79 | github.com/ysmood/leakless v0.8.0 h1:BzLrVoiwxikpgEQR0Lk8NyBN5Cit2b1z+u0mgL4ZJak= 80 | github.com/ysmood/leakless v0.8.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= 81 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 82 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 83 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= 84 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 85 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 86 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 87 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 88 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 89 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 90 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 91 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 92 | golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= 93 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 94 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= 95 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 96 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 98 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 99 | golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 100 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 101 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 102 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 103 | golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 104 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 105 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 106 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 107 | golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= 108 | golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 109 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 110 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 111 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 112 | golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= 113 | golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= 114 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 115 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 116 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 117 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 118 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 119 | golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= 120 | golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 121 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 122 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 123 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 124 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 125 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 126 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 127 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 128 | google.golang.org/appengine v1.6.6 h1:lMO5rYAqUxkmaj76jAkRUvt5JZgFymx/+Q5Mzfivuhc= 129 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 130 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 131 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 132 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 133 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 134 | jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056 h1:6YFJoB+0fUH6X3xU/G2tQqCYg+PkGtnZ5nMR5rpw72g= 135 | jaytaylor.com/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:OxvTsCwKosqQ1q7B+8FwXqg4rKZ/UG9dUW+g/VL2xH4= 136 | -------------------------------------------------------------------------------- /llama2.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "github.com/go-rod/rod" 7 | "github.com/go-rod/stealth" 8 | "time" 9 | ) 10 | 11 | // Set up client of Llama2 (Rod version) 12 | var page_llama2 *rod.Page 13 | var relogin_llama2 = true 14 | var channel_llama2 chan string 15 | 16 | func Llama2() { 17 | channel_llama2 = make(chan string) 18 | defer func() { 19 | if err := recover(); err != nil { 20 | relogin_llama2 = true 21 | } 22 | }() 23 | //page_hc = browser.MustPage("https://huggingface.co/chat") 24 | page_llama2 = stealth.MustPage(browser) 25 | page_llama2.MustNavigate("https://ysharma-explore-llamav2-with-tgi.hf.space") 26 | for i := 1; i <= 30; i++ { 27 | if page_llama2.MustHasX("//textarea[@data-testid='textbox']") { 28 | relogin_llama2 = false 29 | break 30 | } 31 | time.Sleep(time.Second) 32 | } 33 | if relogin_llama2 == true { 34 | sprint("✘ Llama2") 35 | //page_hc.MustPDF("./tmp/HuggingChat✘.pdf") 36 | } 37 | if relogin_llama2 == false { 38 | sprint("✔ Llama2") 39 | for { 40 | select { 41 | case question := <-channel_llama2: 42 | page_llama2.MustActivate() 43 | //fmt.Println("Falcon180 received question...", question) 44 | for i := 1; i <= 20; i++ { 45 | if page_llama2.MustHasX("//textarea[@data-testid='textbox']") { 46 | page_llama2.MustElementX("//textarea[@data-testid='textbox']").MustInput(question) 47 | break 48 | } 49 | time.Sleep(time.Second) 50 | } 51 | //fmt.Println("Falcon180 input typed...") 52 | for i := 1; i <= 20; i++ { 53 | 54 | //if page_llama2.MustHasX("//button[contains(text(), 'Submit')]") { 55 | //page_llama2.MustElementX("//button[contains(text(), 'Submit')]").MustClick() 56 | if page_llama2.MustHasX("//button[@id='component-14']") { 57 | page_llama2.MustElementX("//button[@id='component-14']").MustClick() 58 | break 59 | } 60 | time.Sleep(time.Second) 61 | } 62 | fmt.Println("Llama2 generating...", role) 63 | //page_falcon180.MustActivate() // Sometime three dot to hang 64 | if role == ".all" { 65 | channel_llama2 <- "click_llama2" 66 | } 67 | //// Check Error 68 | //channel_falcon180_check := make(chan string) 69 | //go func() { 70 | // for i := 1; i <= 20; i++ { 71 | // if page_falcon180.MustHasX("//*[contains(text(), 'Too much traffic, please try again')]") { 72 | // channel_falcon180 <- "✘✘ Falcon180, Please check the internet connection and verify login status. Traffic." 73 | // fmt.Println("Falcon180 too much traffic...") 74 | // relogin_falcon180 = true 75 | // close(channel_falcon180_check) 76 | // break 77 | // } 78 | // time.Sleep(1 * time.Second) 79 | // } 80 | //}() 81 | 82 | // stop_icon 83 | var stop_icon_disappear = false 84 | err := rod.Try(func() { 85 | //page_llama2.Timeout(10 * time.Second).MustElementX("//button[contains(text(), 'Stop')]").MustWaitVisible().CancelTimeout() 86 | page_llama2.Timeout(10 * time.Second).MustElementX("//button[@id='component-15']").MustWaitVisible().CancelTimeout() 87 | }) 88 | //fmt.Println("find stop button") 89 | if err == nil { 90 | err = rod.Try(func() { 91 | //page_llama2.Timeout(80 * time.Second).MustElementX("//button[contains(text(), 'Stop')]").MustWaitInvisible().CancelTimeout() 92 | page_llama2.Timeout(80 * time.Second).MustElementX("//button[@id='component-15']").MustWaitInvisible().CancelTimeout() 93 | //fmt.Println("disappear stop button") 94 | }) 95 | if err == nil { 96 | stop_icon_disappear = true 97 | } else { 98 | //fmt.Println("err in find disappear stop button", err) 99 | } 100 | } else { 101 | //fmt.Println("err in find stop button", err) 102 | } 103 | 104 | //if page_llama2.MustHas("span:contains('Error')") { 105 | //fmt.Println("finding Error") 106 | if page_llama2.MustHasX("//span[contains(text(), 'Error')]"){ 107 | 108 | //fmt.Println("find Error") 109 | channel_llama2 <- "✘✘ Llama2, Please check the internet connection and verify login status." 110 | relogin_llama2 = true 111 | } 112 | 113 | 114 | 115 | if stop_icon_disappear == true { 116 | //answer := page_llama2.MustElementX("(//div[@data-testid='bot'])[last()]").MustText() 117 | answer := page_llama2.MustElementX("(//button[@data-testid='bot'])[last()]").MustText() 118 | channel_llama2 <- answer 119 | } else { 120 | channel_llama2 <- "✘✘ Llama2, Please check the internet connection and verify login status." 121 | relogin_llama2 = true 122 | //page_hc.MustPDF("./tmp/HuggingChat✘.pdf") 123 | 124 | } 125 | } 126 | } 127 | } 128 | 129 | } 130 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | # mac amd64 2 | GOOS=windows GOARCH=386 cd ./ryy && go build -o ../vi && cd .. && go build -tags vi -o aih.exe && zip -r ./release/aih_x86_exe.zip aih.exe && rm -rf vi aih.exe # aih_x86.exe 3 | GOOS=windows GOARCH=amd64 cd ./ryy && go build -o ../vi && cd .. && go build -tags vi -o aih.exe && zip -r ./release/aih_amd64_exe.zip aih.exe && rm -rf vi aih.exe # aih_amd64.exe 4 | GOOS=windows GOARCH=arm cd ./ryy && go build -o ../vi && cd .. && go build -tags vi -o aih.exe && zip -r ./release/aih_arm_exe.zip aih.exe && rm -rf vi aih.exe # aih_arm.exe 5 | GOOS=windows GOARCH=arm64 cd ./ryy && go build -o ../vi && cd .. && go build -tags vi -o aih.exe && zip -r ./release/aih_arm64_exe.zip aih.exe && rm -rf vi aih.exe # aih_arm64.exe 6 | GOOS=linux GOARCH=386 cd ./ryy && go build -o ../vi && cd .. && go build -tags vi -o aih && zip -r ./release/aih_linux_x86.zip aih && rm -rf vi aih # aih_linux_x86 7 | GOOS=linux GOARCH=amd64 cd ./ryy && go build -o ../vi && cd .. && go build -tags vi -o aih && zip -r ./release/aih_linux_amd64.zip aih && rm -rf vi aih # aih_linux_amd64 8 | GOOS=linux GOARCH=arm cd ./ryy && go build -o ../vi && cd .. && go build -tags vi -o aih && zip -r ./release/aih_linux_arm.zip aih && rm -rf vi aih # aih_linux_arm 9 | GOOS=linux GOARCH=arm64 cd ./ryy && go build -o ../vi && cd .. && go build -tags vi -o aih && zip -r ./release/aih_linux_arm64.zip aih && rm -rf vi aih # aih_linux_arm64 10 | #GOOS=darwin GOARCH=386 cd ./ryy && go build -o ../vi && cd .. && go build -tags vi -o aih && zip -r ./release/aih_mac_x86.zip aih && rm -rf vi aih # aih_mac_x86 11 | GOOS=darwin GOARCH=amd64 cd ./ryy && go build -o ../vi && cd .. && go build -tags vi -o aih && zip -r ./release/aih_mac_amd64.zip aih && rm -rf vi aih # aih_mac_amd64 12 | GOOS=darwin GOARCH=arm64 cd ./ryy && go build -o ../vi && cd .. && go build -tags vi -o aih && zip -r ./release/aih_mac_arm64.zip aih && rm -rf vi aih # aih_mac_arm64 13 | -------------------------------------------------------------------------------- /release/upload.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "github.com/google/go-github/github" 10 | "golang.org/x/oauth2" 11 | ) 12 | 13 | func main() { 14 | var ghp_token string 15 | fmt.Printf("Please input ghp_token: ") 16 | fmt.Scanf("%s", & ghp_token) 17 | //fmt.Println("ghp_token: ", ghp_token) 18 | 19 | ctx := context.Background() 20 | 21 | // Authenticate with GitHub using a personal access token 22 | ts := oauth2.StaticTokenSource( 23 | &oauth2.Token{AccessToken: ghp_token}, 24 | ) 25 | tc := oauth2.NewClient(ctx, ts) 26 | client := github.NewClient(tc) 27 | 28 | // Set the owner and repository for the release 29 | owner := "Databingo" 30 | repo := "aih" 31 | 32 | // Create a new release 33 | release, _, err := client.Repositories.CreateRelease(ctx, owner, repo, &github.RepositoryRelease{ 34 | TagName: github.String("v0.2.6"), 35 | TargetCommitish: github.String("master"), 36 | Name: github.String("Release v0.2.6"), 37 | Body: github.String("Welcome to Aih! Fix bard.go and chatgpt.go for new changes. Disable falcon and claude temporarily."), 38 | Draft: github.Bool(false), 39 | Prerelease: github.Bool(false), 40 | }) 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | 45 | // Upload binary zip files to the release 46 | files := []string{ "aih_amd64_exe.zip", "aih_arm64_exe.zip", "aih_arm_exe.zip", "aih_linux_amd64.zip", "aih_linux_arm.zip", "aih_linux_arm64.zip", "aih_linux_x86.zip", "aih_mac_amd64.zip", "aih_mac_arm64.zip", "aih_x86_exe.zip"} 47 | for _, file := range files { 48 | // Read the file contents 49 | _, err := ioutil.ReadFile(file) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | 54 | // Create the file on GitHub 55 | fileContent, err := os.Open(file) 56 | if err != nil { 57 | log.Fatal(err) 58 | } 59 | defer fileContent.Close() 60 | 61 | _, _, err = client.Repositories.UploadReleaseAsset(ctx, owner, repo, release.GetID(), &github.UploadOptions{ 62 | Name: file, 63 | }, fileContent) 64 | if err != nil { 65 | log.Fatal(err) 66 | } 67 | 68 | fmt.Printf("Uploaded %s to release %d\n", file, release.GetID()) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ryy/.gitignore: -------------------------------------------------------------------------------- 1 | ry 2 | -------------------------------------------------------------------------------- /ryy/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 kiasaki 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /ryy/Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | go build -i -v -o ry . 3 | 4 | run: build 5 | ./ry 6 | 7 | install: 8 | install ./ry /usr/local/bin/ry 9 | 10 | clean: 11 | rm ry 12 | 13 | .PHONY: build run clean 14 | -------------------------------------------------------------------------------- /ryy/README.md: -------------------------------------------------------------------------------- 1 | # ry 2 | 3 | _A simple modal text editor, written in Go_ 4 | 5 | or 6 | 7 | _This is my editor. There are many like it, but this one is mine._ 8 | 9 | ### installing 10 | 11 | To build and install `ry` on your computer simply run 12 | `go get github.com/kiasaki/ry`. 13 | 14 | ### developing 15 | 16 | ```bash 17 | make run # builds and runs ry 18 | ``` 19 | 20 | ### features 21 | 22 | `ry` is a text editor aiming to provide an editing environment similar to `vim` 23 | in terms of key bindings and modal editing while being as easily extended 24 | as `emacs`. It's is built with the day-to-day usage of the original author in 25 | mind but hopefully flexible enough for anybody with Vim experience to adopt and 26 | mold to their image. 27 | 28 | **Currently implemented keybindings:** 29 | 30 | - Normal Mode 31 | - C-q Quits editor 32 | - : Enters command mode 33 | - C-c Cancels keys entered 34 | - C-g Cancels keys entered 35 | - ESC ESC Cancels keys entered 36 | - h Moves cursor left 37 | - h Moves cursor right 38 | - j Moves cursor down 39 | - k Moves cursor up 40 | - 0 Moves cursor to the beginning of the line 41 | - $ Moves cursor to the beginning of the line 42 | - g g Moves to the beginning of the buffer 43 | - G Moves to the end of the buffer 44 | - C-u Move 15 lines up 45 | - C-d Moves 15 lines down 46 | - z z Centers current line in view 47 | - w Moves forward to next beginning of a word 48 | - e Moves forward to next end of a word 49 | - b Moves backwards to next beginning of a word 50 | - i Enters insert-mode 51 | - I Enters insert-mode at beginning of line 52 | - a Enters insert-mode then moves right 1 character 53 | - A Enters insert-mode then moves to the end of the line 54 | - o Enters insert-mode and creates a new line under the current one 55 | - O Enters insert-mode and creates a new line on top of the current one 56 | - u Undo last change 57 | - C-r Redo last change 58 | - x Delete char under cursor 59 | - d d Deletes line under cursor 60 | - y y Copies line under cursor 61 | - p Pastes from clipboard 62 | - m $alpha Set mark at cursor 63 | - ' $alpha Jump to mark 64 | - v Enter visual mode 65 | - V Enter visual line mode 66 | - C-w s Splits buffer horizontally 67 | - C-w v Splits buffer vertically 68 | - C-w h Move to the window to the left 69 | - C-w j Move to the window to the bottom 70 | - C-w k Move to the window to the top 71 | - C-w l Move to the window to the right 72 | - SPC b Runs `buffers` command 73 | - SPC f Runs `edit` command on current file's directory 74 | - SPC n Runs `clearsearch` command 75 | - Insert mode 76 | - $any Inserts character at cursor's position 77 | - BAK Deletes character to the left 78 | - RET Inserts a new line at cursor position 79 | - ESC Enters normal mode 80 | - Prompt mode 81 | - $any Inserts character 82 | - BAK Deletes character 83 | - C-c Enters normal mode 84 | - ESC Enters normal mode 85 | - RET Execute command and go back to normal mode 86 | - C-u Clear entered command 87 | - Visual mode 88 | - ESC Exit visual mode 89 | - y Yank selection 90 | - d Delete selection 91 | - p Paste selection 92 | - Buffers mode 93 | - q Close buffer 94 | - RET Open selected buffer in current window 95 | - Directory mode 96 | - q Close buffer 97 | - RET Open selected file in current window 98 | 99 | **Currently implemented commands:** 100 | 101 | - `edit ` (aliased as `e`) Edit a file in a new buffer (shows file selector on directories) 102 | - `write ` (aliased as `w`) Write buffer to disk, optionally setting it's path 103 | - `quit` (aliased as `q`) Close current buffer (making sure it's saved before) 104 | - `quit!` (aliased as `q!`) Close current buffer (ignoring unsaved changes) 105 | - `writequit` (aliased as `wq`) Writes buffer to disk then closes it 106 | - `clearsearch (aliased as `cs`) Hides search result highlights 107 | - `buffers` (aliased as `b`) Shows a list of buffers in current window 108 | 109 | ### screenshot 110 | 111 | ![](https://raw.githubusercontent.com/kiasaki/ry/master/screenshot.png) 112 | 113 | ### license 114 | 115 | See `LICENSE` file. 116 | 117 | -------------------------------------------------------------------------------- /ryy/TODO.md: -------------------------------------------------------------------------------- 1 | # Todo 2 | 3 | - Autocomplete (based on open buffers tokens) 4 | - Buffer related commands/keybindings (language modes?) 5 | - ~Command mode (q,w,o,e,wq,!,wqall)~ 6 | - ~Marks~ 7 | - ~Visual mode (+ visual line)~ 8 | - ~Incremental Search~ 9 | - ~Buffer list/switch mode~ 10 | - ~File tree mode~ 11 | 12 | **Post 1.0** 13 | 14 | - Syntax highlithing 15 | - Settings 16 | - Scripting / user settings 17 | - Fuzzy file search mode 18 | - Recursive grep mode 19 | - Line wrapping 20 | - Windows 21 | - Color schemes 22 | - Shell mode 23 | - Ensure UTF-8 works 24 | - Help/Tutorial 25 | - Fringe for error reporting / plugins 26 | 27 | # The big list 28 | 29 | - [ ] Auto indent 30 | - [ ] Custom bindings 31 | - [ ] Line numbers 32 | - [ ] Search and replace 33 | - [ ] Search 34 | - [ ] Replace 35 | - [ ] Tests 36 | - [ ] Error handling 37 | - [ ] Fatal 38 | - [ ] Script/Runtime 39 | - [ ] User 40 | - [ ] Unicode support 41 | - [ ] Command execution 42 | - [ ] Movement keys 43 | - [ ] Visual mode 44 | - [ ] Normal 45 | - [ ] Line 46 | - [ ] Help 47 | - [ ] General 48 | - [ ] Per command 49 | - [ ] Tutorial 50 | - [ ] Options/Configuration 51 | - [ ] Save/Load 52 | - [ ] Tabs to space 53 | - [ ] Tab size 54 | - [ ] Color scheme 55 | - [ ] Undo/Redo 56 | - [ ] Clipboard 57 | - [ ] Copy 58 | - [ ] Paste 59 | - [ ] Cut 60 | - [ ] Registers 61 | - [ ] Macros 62 | - [ ] Syntax highlighting 63 | - [ ] Color schemes 64 | 65 | # Vim's Perl Interface for inspiration: 66 | 67 | ``` 68 | VIM::Msg({msg}, {group}?) 69 | VIM::SetOption({arg}) Sets a vim option. 70 | VIM::Buffers([{bn}...]) With no arguments, returns a list of all the buffers. 71 | VIM::Windows([{wn}...]) With no arguments, returns a list of all the windows. 72 | VIM::DoCommand({cmd}) Executes Ex command {cmd}. 73 | VIM::Eval({expr}) Evaluates {expr} and returns (success, val). 74 | Window->SetHeight({height}) 75 | Window->Cursor({row}?, {col}?) 76 | Window->Buffer() 77 | Buffer->Name() Returns the filename for the Buffer. 78 | Buffer->Number() Returns the number of the Buffer. 79 | Buffer->Count() Returns the number of lines in the Buffer. 80 | Buffer->Get({lnum}, {lnum}?, ...) 81 | Buffer->Delete({lnum}, {lnum}?) 82 | Buffer->Append({lnum}, {line}, {line}?, ...) 83 | Buffer->Set({lnum}, {line}, {line}?, ...) 84 | $main::curwin 85 | $main::curbuf 86 | ``` 87 | 88 | # Some Emacs interface functions 89 | 90 | ...to consider implementing? 91 | 92 | - `(redraw-display)` 93 | - `(redisplay force?)` 94 | - `(message format &rest args)` 95 | - `(with-temp-message message &rest body)` Show message, executes body, removes message, returns body result 96 | - `(current-message)` 97 | - `(make-progress-reporter message &optional min-value max-value current-value min-change min-time)` 98 | - `(progress-reporter-update reporter &optional value)` 99 | - `(progress-reporter-done reporter)` 100 | - `(messages-buffer)` 101 | - `(yes-or-no-p)` 102 | - `cursor-in-echo-area` 103 | - `(format)` 104 | - `(display-warning type message &optional level)` Levels being: emergency, error, warning, debug 105 | - Handle hiding/make buffer sections invisible (for folding) 106 | - `before-init-time` 107 | - `after-init-time` 108 | - `(save-excursion)` 109 | 110 | -------------------------------------------------------------------------------- /ryy/action.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type ActionType int 4 | 5 | const ( 6 | ActionTypeInsert ActionType = 1 7 | ActionTypeRemove = -1 8 | ) 9 | 10 | type Action struct { 11 | Typ ActionType 12 | Loc *Location 13 | Data []rune 14 | } 15 | 16 | func NewAction(typ ActionType, loc *Location, data []rune) *Action { 17 | return &Action{Typ: typ, Loc: loc, Data: data} 18 | } 19 | 20 | func (a *Action) Apply(b *Buffer) { 21 | a.Do(b, a.Typ) 22 | b.Modified = true 23 | hook_trigger_buffer("modified", b) 24 | } 25 | 26 | func (a *Action) Revert(b *Buffer) { 27 | a.Do(b, -a.Typ) 28 | b.Modified = true 29 | hook_trigger_buffer("modified", b) 30 | } 31 | 32 | func (a *Action) Do(b *Buffer, typ ActionType) { 33 | if typ == ActionTypeInsert { 34 | a.Insert(b) 35 | } else { 36 | a.Remove(b) 37 | } 38 | } 39 | 40 | func (a *Action) Insert(b *Buffer) { 41 | c, l := a.Loc.Char, a.Loc.Line 42 | for i := len(a.Data) - 1; i >= 0; i-- { 43 | ch := a.Data[i] 44 | if ch == '\n' { 45 | rest := append([]rune(nil), b.Data[l][c:]...) 46 | b.Data[l] = b.Data[l][:c] 47 | b.Data = append(b.Data[:l+1], 48 | append([][]rune{rest}, b.Data[l+1:]...)...) 49 | } else { 50 | b.Data[l] = append(b.Data[l][:c], 51 | append([]rune{ch}, b.Data[l][c:]...)...) 52 | } 53 | } 54 | } 55 | 56 | func (a *Action) Remove(b *Buffer) { 57 | n := len(a.Data) 58 | c, l := a.Loc.Char, a.Loc.Line 59 | removed := []rune{} 60 | for i := 0; i < n; i++ { 61 | removed = append(removed, b.CharAt(l, c)) 62 | if b.CharAt(l, c) == '\n' { 63 | if len(b.Data)-1 == l { 64 | a.Data = removed 65 | return 66 | } 67 | b.Data[l] = append(b.Data[l], b.Data[l+1]...) 68 | b.Data = append(b.Data[:l+1], b.Data[l+2:]...) 69 | } else { 70 | b.Data[l] = append(b.Data[l][:c], b.Data[l][c+1:]...) 71 | } 72 | } 73 | a.Data = removed 74 | } 75 | -------------------------------------------------------------------------------- /ryy/buffer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type Location struct { 12 | Line int 13 | Char int 14 | } 15 | 16 | func NewLocation(l, c int) *Location { 17 | return &Location{Line: l, Char: c} 18 | } 19 | 20 | func orderLocations(l1, l2 *Location) (*Location, *Location) { 21 | if l1.Line < l2.Line { 22 | return l1, l2 23 | } else if l1.Line > l2.Line { 24 | return l2, l1 25 | } else { 26 | if l1.Char > l2.Char { 27 | return l2, l1 28 | } else { 29 | return l1, l2 30 | } 31 | } 32 | } 33 | 34 | func (l1 *Location) Equal(l2 *Location) bool { 35 | return l1.Line == l2.Line && l1.Char == l2.Char 36 | } 37 | 38 | func (l1 *Location) Before(l2 *Location) bool { 39 | ol1, _ := orderLocations(l1, l2) 40 | return l1 == ol1 41 | } 42 | 43 | func (l1 *Location) After(l2 *Location) bool { 44 | _, ol2 := orderLocations(l1, l2) 45 | return l1 == ol2 46 | } 47 | 48 | func (loc *Location) Clone() *Location { 49 | return &Location{Line: loc.Line, Char: loc.Char} 50 | } 51 | 52 | type CharRange struct { 53 | Beg int 54 | End int 55 | } 56 | 57 | func NewCharRange(b, e int) *CharRange { 58 | return &CharRange{b, e} 59 | } 60 | 61 | type Buffer struct { 62 | Data [][]rune 63 | History []*Action 64 | HistoryIndex int 65 | Name string 66 | Path string 67 | Modified bool 68 | Cursor *Location 69 | Modes []string 70 | LastRenderWidth int 71 | LastRenderHeight int 72 | } 73 | 74 | func NewBuffer(name string, path string) *Buffer { 75 | b := &Buffer{ 76 | Data: [][]rune{{}}, 77 | History: []*Action{}, 78 | HistoryIndex: -1, 79 | Modified: false, 80 | Cursor: NewLocation(0, 0), 81 | Modes: []string{}, 82 | } 83 | 84 | if path == "" { 85 | // TODO ensure uniqueness 86 | b.Name = name 87 | } else { 88 | b.SetPath(path) 89 | // TODO ensure uniqueness 90 | b.Name = name 91 | } 92 | 93 | return b 94 | } 95 | 96 | func (b *Buffer) IsInMode(name string) bool { 97 | for _, n := range b.Modes { 98 | if n == name { 99 | return true 100 | } 101 | } 102 | return false 103 | } 104 | 105 | func (b *Buffer) CharAt(l, c int) rune { 106 | line := b.Data[l] 107 | if c < 0 { 108 | return rune(0) 109 | } else if c < len(line) { 110 | return line[c] 111 | } else { 112 | return '\n' 113 | } 114 | } 115 | 116 | func (b *Buffer) GetLine(l int) []rune { 117 | return b.Data[l] 118 | } 119 | 120 | func (b *Buffer) CharAtLeft() rune { 121 | return b.CharAt(b.Cursor.Line, b.Cursor.Char-1) 122 | } 123 | 124 | func (b *Buffer) CharUnderCursor() rune { 125 | return b.CharAt(b.Cursor.Line, b.Cursor.Char) 126 | } 127 | 128 | func (b *Buffer) WordUnderCursor() []rune { 129 | ch := b.CharUnderCursor() 130 | if !isWord(ch) { 131 | return nil 132 | } 133 | line := b.GetLine(b.Cursor.Line) 134 | cstart := b.Cursor.Char 135 | for cstart > 0 && isWord(b.CharAt(b.Cursor.Line, cstart-1)) { 136 | cstart-- 137 | } 138 | cend := cstart 139 | for cend < len(line) && isWord(b.CharAt(b.Cursor.Line, cend+1)) { 140 | cend++ 141 | } 142 | return line[cstart : cend+1] 143 | } 144 | 145 | func (b *Buffer) FirstLine() bool { 146 | return b.Cursor.Line == 0 147 | } 148 | 149 | func (b *Buffer) LastLine() bool { 150 | return b.Cursor.Line == len(b.Data)-1 151 | } 152 | 153 | func (b *Buffer) MoveTo(c, l int) { 154 | b.Cursor.Line = max(min(l, len(b.Data)-1), 0) 155 | b.Cursor.Char = max(min(c, len(b.Data[b.Cursor.Line])), 0) 156 | hook_trigger_buffer("moved", b) 157 | } 158 | 159 | func (b *Buffer) Move(c, l int) { 160 | b.MoveTo(b.Cursor.Char+c, b.Cursor.Line+l) 161 | } 162 | 163 | func (b *Buffer) MoveWordForward() bool { 164 | for { 165 | c := b.CharUnderCursor() 166 | if c == '\n' { 167 | if b.LastLine() { 168 | return false 169 | } else { 170 | b.MoveTo(0, b.Cursor.Line+1) 171 | break 172 | } 173 | } 174 | 175 | for isWord(c) && c != '\n' { 176 | b.Move(1, 0) 177 | c = b.CharUnderCursor() 178 | } 179 | 180 | if c == '\n' { 181 | continue 182 | } 183 | break 184 | } 185 | 186 | c := b.CharUnderCursor() 187 | for !isWord(c) && c != '\n' { 188 | b.Move(1, 0) 189 | c = b.CharUnderCursor() 190 | } 191 | 192 | return true 193 | } 194 | 195 | func (b *Buffer) MoveWordEndForward() bool { 196 | b.Move(1, 0) 197 | for { 198 | c := b.CharUnderCursor() 199 | if c == '\n' { 200 | if b.LastLine() { 201 | return false 202 | } else { 203 | b.MoveTo(0, b.Cursor.Line+1) 204 | break 205 | } 206 | } 207 | 208 | for !isWord(c) && c != '\n' { 209 | b.Move(1, 0) 210 | c = b.CharUnderCursor() 211 | } 212 | 213 | if c == '\n' { 214 | continue 215 | } 216 | break 217 | } 218 | 219 | c := b.CharUnderCursor() 220 | for isWord(c) && c != '\n' { 221 | b.Move(1, 0) 222 | c = b.CharUnderCursor() 223 | } 224 | b.Move(-1, 0) 225 | 226 | return true 227 | } 228 | 229 | func (b *Buffer) MoveWordBackward() bool { 230 | b.Move(-1, 0) 231 | for { 232 | c := b.CharUnderCursor() 233 | if b.Cursor.Char == 0 { 234 | if b.FirstLine() { 235 | return false 236 | } else { 237 | b.MoveTo(len(b.Data[b.Cursor.Line-1]), b.Cursor.Line-1) 238 | continue 239 | } 240 | } 241 | 242 | for !isWord(c) && b.Cursor.Char != 0 { 243 | b.Move(-1, 0) 244 | c = b.CharUnderCursor() 245 | } 246 | 247 | if b.Cursor.Char == 0 { 248 | continue 249 | } 250 | break 251 | } 252 | 253 | c := b.CharUnderCursor() 254 | for isWord(c) && b.Cursor.Char != 0 { 255 | b.Move(-1, 0) 256 | c = b.CharUnderCursor() 257 | } 258 | b.Move(1, 0) 259 | 260 | return true 261 | } 262 | 263 | func (b *Buffer) Insert(data []rune) { 264 | a := NewAction(ActionTypeInsert, b.Cursor.Clone(), data) 265 | b.HistoryIndex++ 266 | b.History = tryMergeHistory(b.History[:b.HistoryIndex], a) 267 | a.Apply(b) 268 | } 269 | 270 | func (b *Buffer) RemoveAt(loc *Location, n int) []rune { 271 | a := NewAction(ActionTypeRemove, loc.Clone(), make([]rune, n)) 272 | b.HistoryIndex++ 273 | b.History = tryMergeHistory(b.History[:b.HistoryIndex], a) 274 | a.Apply(b) 275 | return a.Data 276 | } 277 | 278 | func (b *Buffer) Remove(n int) []rune { 279 | return b.RemoveAt(b.Cursor, n) 280 | } 281 | 282 | func (b *Buffer) Undo() { 283 | if b.HistoryIndex >= 0 && b.HistoryIndex != -1 { 284 | b.History[b.HistoryIndex].Revert(b) 285 | b.HistoryIndex-- 286 | } else { 287 | message("Noting to undo!") 288 | } 289 | } 290 | 291 | func (b *Buffer) Redo() { 292 | if b.HistoryIndex+1 < len(b.History) && len(b.History) > b.HistoryIndex+1 { 293 | b.HistoryIndex++ 294 | b.History[b.HistoryIndex].Apply(b) 295 | } else { 296 | message("Noting to redo!") 297 | } 298 | } 299 | 300 | func (b *Buffer) SetPath(path string) { 301 | var err error 302 | b.Path, err = filepath.Abs(path) 303 | if err != nil { 304 | b.Path = filepath.Clean(path) 305 | } 306 | b.Name = "" 307 | name := filepath.Base(b.Path) 308 | 309 | i := 1 310 | checkName: 311 | for _, b2 := range buffers { 312 | if b2.Name == name { 313 | b.Name = name + " " + strconv.Itoa(i) 314 | i++ 315 | goto checkName 316 | } 317 | } 318 | if b.Name == "" { 319 | b.Name = name 320 | } 321 | } 322 | 323 | func (b *Buffer) AddMode(name string) { 324 | if b.IsInMode(name) { 325 | return 326 | } 327 | b.Modes = append(b.Modes, name) 328 | } 329 | 330 | func (b *Buffer) RemoveMode(name string) { 331 | for i, n := range b.Modes { 332 | if n == name { 333 | b.Modes = append(b.Modes[:i], b.Modes[i+1:]...) 334 | return 335 | } 336 | } 337 | } 338 | 339 | func (b *Buffer) Contents() string { 340 | ret := "" 341 | for _, line := range b.Data { 342 | ret += string(line) + "\n" 343 | } 344 | return ret 345 | } 346 | 347 | func (b *Buffer) NicePath() string { 348 | return strings.Replace(b.Path, os.Getenv("HOME"), "~", -1) 349 | } 350 | 351 | func (b *Buffer) Save() { 352 | if b.Path == "" { 353 | messageError("Can't save a buffer without a path.") 354 | return 355 | } 356 | err := ioutil.WriteFile(b.Path, []byte(b.Contents()), 0666) 357 | if err != nil { 358 | messageError("Error saving buffer: " + err.Error()) 359 | } else { 360 | b.Modified = false 361 | message("Buffer written to '" + b.NicePath() + "'") 362 | } 363 | } 364 | 365 | func tryMergeHistory(al []*Action, a *Action) []*Action { 366 | // TODO save end location on actions so that we can merge them here 367 | return append(al, a) 368 | } 369 | -------------------------------------------------------------------------------- /ryy/clipboard.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | zclip "github.com/zyedidia/clipboard" 5 | ) 6 | 7 | func clipboardGet(register rune) []rune { 8 | if register == defaultClipboard { 9 | if value, err := zclip.ReadAll("clipboard"); err == nil { 10 | return []rune(value) 11 | } 12 | } 13 | if value, ok := clipboards[register]; ok { 14 | return value 15 | } 16 | return []rune{} 17 | } 18 | 19 | func clipboardSet(register rune, value []rune) { 20 | if register == defaultClipboard { 21 | if err := zclip.WriteAll(string(value), "clipboard"); err != nil { 22 | messageError("Error clipboard_get: " + err.Error()) 23 | clipboards[register] = value 24 | } 25 | } else { 26 | clipboards[register] = value 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ryy/commands.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "os" 6 | "path/filepath" 7 | "sort" 8 | "strings" 9 | // "fmt" 10 | ) 11 | 12 | func openBufferFromFile(path string) *Buffer { 13 | if fileInfo, err := os.Stat(path); os.IsNotExist(err) || err != nil { 14 | return openBufferNamed(filepath.Base(path)) 15 | } else { 16 | if fileInfo.IsDir() { 17 | file, err := os.Open(path) 18 | if err != nil { 19 | messageError("Error opening directory: " + err.Error()) 20 | return nil 21 | } 22 | files, err := file.Readdir(0) 23 | if err != nil { 24 | messageError("Error opening directory: " + err.Error()) 25 | return nil 26 | } 27 | buf := NewBuffer(filepath.Base(path), path) 28 | buf.Data = [][]rune{} 29 | file_names := []string{} 30 | for _, file_info := range files { 31 | if file_info.IsDir() { 32 | file_names = append(file_names, " "+file_info.Name()) 33 | } else { 34 | file_names = append(file_names, file_info.Name()) 35 | } 36 | } 37 | file_names = append(file_names, " ..") 38 | sort.Strings(file_names) 39 | for _, file_name := range file_names { 40 | if file_name[0] == ' ' { // is dir 41 | buf.Data = append(buf.Data, []rune(file_name[1:]+"/")) 42 | } else { 43 | buf.Data = append(buf.Data, []rune(file_name)) 44 | } 45 | } 46 | buf.AddMode("directory") 47 | buffers = append(buffers, buf) 48 | hook_trigger_buffer("modified", buf) 49 | return buf 50 | } 51 | } 52 | 53 | buf := NewBuffer(filepath.Base(path), path) 54 | if buf.Path != "" { 55 | contents, err := ioutil.ReadFile(buf.Path) 56 | if err != nil { 57 | messageError("Error reading file '" + buf.NicePath() + "'") 58 | return nil 59 | } 60 | buf.Data = [][]rune{} 61 | for _, line := range strings.Split(string(contents), "\n") { 62 | buf.Data = append(buf.Data, []rune(line)) 63 | } 64 | if len(buf.Data) > 1 { 65 | buf.Data = buf.Data[:len(buf.Data)-1] 66 | } 67 | } 68 | buffers = append(buffers, buf) 69 | hook_trigger_buffer("modified", buf) 70 | return buf 71 | } 72 | 73 | func openBufferNamed(name string) *Buffer { 74 | buf := NewBuffer(name, "") 75 | buffers = append(buffers, buf) 76 | hook_trigger_buffer("modified", buf) 77 | return buf 78 | } 79 | 80 | // TODO check if is shown first, then if not create split not replace 81 | func showBuffer(buffer_name string) *Buffer { 82 | for _, b := range buffers { 83 | if b.Name == buffer_name { 84 | if currentViewTree.Leaf.Buf == b { 85 | return b // already shown 86 | } 87 | currentViewTree = NewViewTreeLeaf(nil, NewView(b)) 88 | rootViewTree = currentViewTree 89 | return b 90 | } 91 | } 92 | return nil 93 | } 94 | 95 | func closeBuffer(b *Buffer) { 96 | for i, b2 := range buffers { 97 | if b == b2 { 98 | buffers = append(buffers[:i], buffers[i+1:]...) 99 | break 100 | } 101 | } 102 | } 103 | 104 | func selectAvailableBuffer(closeIfNone bool) { 105 | if len(buffers) == 0 { 106 | if closeIfNone { 107 | // TODO Use method here (don't handcode screen.Fini()) 108 | screen.Fini() 109 | os.Exit(0) 110 | } 111 | } else { 112 | currentViewTree = NewViewTreeLeaf(nil, NewView(buffers[0])) 113 | rootViewTree = currentViewTree 114 | } 115 | } 116 | 117 | func closeCurrentBuffer(force bool) { 118 | b := currentViewTree.Leaf.Buf 119 | if b.Modified && !force { 120 | messageError("Save buffer before closing it.") 121 | return 122 | } 123 | closeBuffer(b) 124 | selectAvailableBuffer(true) 125 | } 126 | 127 | func findBuffer(name string) *Buffer { 128 | for _, b := range buffers { 129 | if b.Name == name { 130 | return b 131 | } 132 | } 133 | return nil 134 | } 135 | 136 | var commands = map[string]func([]string){} 137 | var commandAliases = map[string]string{} 138 | 139 | func runCommand(args []string) { 140 | if len(args) == 0 { 141 | messageError("No command given!") 142 | return 143 | } 144 | command_name := args[0] 145 | if full_command_name, ok := commandAliases[command_name]; ok { 146 | command_name = full_command_name 147 | } 148 | if c, ok := commands[command_name]; ok { 149 | c(args) 150 | } else { 151 | messageError("No command named '" + command_name + "'") 152 | } 153 | } 154 | 155 | func addCommand(name string, fn func([]string)) { 156 | commands[name] = fn 157 | } 158 | func addAlias(alias, name string) { 159 | commandAliases[alias] = name 160 | } 161 | 162 | func initCommands() { 163 | addCommand("quit", func(args []string) { 164 | ///// 165 | screen.Fini() 166 | os.Exit(0) 167 | ///// 168 | //closeCurrentBuffer(false) 169 | closeCurrentBuffer(true) 170 | }) 171 | addAlias("q", "quit") 172 | addCommand("quit!", func(args []string) { 173 | closeCurrentBuffer(false) 174 | }) 175 | addAlias("q!", "quit!") 176 | addCommand("write", func(args []string) { 177 | b := currentViewTree.Leaf.Buf 178 | if len(args) > 1 { 179 | b.SetPath(args[1]) 180 | } 181 | b.Save() 182 | }) 183 | addAlias("w", "write") 184 | //////// 185 | addCommand("wai", func(args []string) { 186 | b := currentViewTree.Leaf.Buf 187 | b.SetPath("./.quest.txt") 188 | b.Save() 189 | //fmt.Println("close") 190 | screen.Fini() 191 | os.Exit(0) 192 | //closeCurrentBuffer(false) 193 | }) 194 | addAlias("ai", "wai") 195 | //////// 196 | addCommand("edit", func(args []string) { 197 | if len(args) < 2 { 198 | messageError("Can't open buffer without a name or file path.") 199 | } else { 200 | path, err := filepath.Abs(args[1]) 201 | if err != nil { 202 | path = args[1] 203 | } 204 | if b := openBufferFromFile(path); b != nil { 205 | showBuffer(b.Name) 206 | } 207 | } 208 | 209 | }) 210 | addAlias("e", "edit") 211 | addAlias("o", "edit") 212 | addCommand("writequit", func(args []string) { 213 | runCommand([]string{"write"}) 214 | runCommand([]string{"quit"}) 215 | }) 216 | addAlias("wq", "writequit") 217 | addCommand("buffers", func(args []string) { 218 | var b *Buffer 219 | if b = findBuffer("*buffers*"); b == nil { 220 | b = openBufferNamed("*buffers*") 221 | b.AddMode("buffers") 222 | } 223 | b.Data = [][]rune{} 224 | for _, buf := range buffers { 225 | if buf.Name != "*buffers*" { 226 | b.Data = append(b.Data, []rune(buf.Name)) 227 | } 228 | } 229 | hook_trigger_buffer("modified", b) 230 | showBuffer(b.Name) 231 | }) 232 | addAlias("b", "buffers") 233 | } 234 | -------------------------------------------------------------------------------- /ryy/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var ( 4 | config map[string]interface{} 5 | ) 6 | 7 | func initConfig() { 8 | config = map[string]interface{}{ 9 | "tab_width": float64(4), 10 | "tab_to_spaces": true, 11 | } 12 | } 13 | 14 | func configGet(key string, b *Buffer) string { 15 | if v, ok := config[key]; ok { 16 | if vv, ok := v.(string); ok { 17 | return vv 18 | } 19 | } 20 | return "" 21 | } 22 | 23 | func configGetBool(key string, b *Buffer) bool { 24 | if v, ok := config[key]; ok { 25 | if vv, ok := v.(bool); ok { 26 | return vv 27 | } 28 | } 29 | return false 30 | } 31 | 32 | func configGetNumber(key string, b *Buffer) float64 { 33 | if v, ok := config[key]; ok { 34 | if vv, ok := v.(float64); ok { 35 | return vv 36 | } 37 | } 38 | return 0 39 | } 40 | 41 | func configSet(key string, value interface{}) { 42 | config[key] = value 43 | } 44 | -------------------------------------------------------------------------------- /ryy/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Databingo/ryy 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/gdamore/encoding v1.0.0 7 | github.com/gdamore/tcell v1.3.0 8 | github.com/go-errors/errors v1.0.0 9 | github.com/kr/pty v1.1.1 10 | github.com/lucasb-eyer/go-colorful v1.0.3 11 | github.com/mattn/go-runewidth v0.0.8 12 | github.com/zyedidia/clipboard v0.0.0-20180208191628-4611e809d8b1 13 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae // indirect 14 | golang.org/x/text v0.3.2 15 | ) 16 | -------------------------------------------------------------------------------- /ryy/go.sum: -------------------------------------------------------------------------------- 1 | github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= 2 | github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635 h1:hheUEMzaOie/wKeIc1WPa7CDVuIO5hqQxjS+dwTQEnI= 3 | github.com/gdamore/encoding v0.0.0-20151215212835-b23993cbb635/go.mod h1:yrQYJKKDTrHmbYxI7CYi+/hbdiDT2m4Hj+t0ikCjsrQ= 4 | github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= 5 | github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= 6 | github.com/gdamore/tcell v1.0.0 h1:oaly4AkxvDT5ffKHV/n4L8iy6FxG2QkAVl0M6cjryuE= 7 | github.com/gdamore/tcell v1.0.0/go.mod h1:tqyG50u7+Ctv1w5VX67kLzKcj9YXR/JSBZQq/+mLl1A= 8 | github.com/gdamore/tcell v1.3.0 h1:r35w0JBADPZCVQijYebl6YMWWtHRqVEGt7kL2eBADRM= 9 | github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= 10 | github.com/go-errors/errors v1.0.0 h1:2G1gYpeHw4GhLet4Ebp5q9wpnSCAOJNTiJq+I3wJV5I= 11 | github.com/go-errors/errors v1.0.0/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= 12 | github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw= 13 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 14 | github.com/lucasb-eyer/go-colorful v0.0.0-20170903184257-231272389856 h1:r+WvXmgROttp7pckv7TPN7OCUEPXmvhRklOOsL2iPPc= 15 | github.com/lucasb-eyer/go-colorful v0.0.0-20170903184257-231272389856/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4= 16 | github.com/lucasb-eyer/go-colorful v1.0.2 h1:mCMFu6PgSozg9tDNMMK3g18oJBX7oYGrC09mS6CXfO4= 17 | github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= 18 | github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= 19 | github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 20 | github.com/mattn/go-runewidth v0.0.2 h1:UnlwIPBGaTZfPQ6T1IGzPI0EkYAQmT9fAEJ/poFC63o= 21 | github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 22 | github.com/mattn/go-runewidth v0.0.4 h1:2BvfKmzob6Bmd4YsL0zygOqfdFnK7GR4QL06Do4/p7Y= 23 | github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= 24 | github.com/mattn/go-runewidth v0.0.8 h1:3tS41NlGYSmhhe/8fhGRzc+z3AYCw1Fe1WAyLuujKs0= 25 | github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 26 | github.com/zyedidia/clipboard v0.0.0-20180208191628-4611e809d8b1 h1:tG+Ld7OcsC4GGkbTt/YIQGW3qfKWtSPI+4lenBFO7q0= 27 | github.com/zyedidia/clipboard v0.0.0-20180208191628-4611e809d8b1/go.mod h1:WDk3p8GiZV9+xFWlSo8qreeoLhW6Ik692rqXk+cNeRY= 28 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10= 29 | golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 30 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae h1:/WDfKMnPU+m5M4xB+6x4kaepxRw6jWvR5iDRdvjHgy8= 31 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 32 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 33 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 34 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 35 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 36 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 37 | -------------------------------------------------------------------------------- /ryy/highlighting.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gdamore/tcell" 7 | ) 8 | 9 | var ( 10 | style_maps = map[*Buffer][][]tcell.Style{} 11 | highlighting_reserved_words = []string{ 12 | "func", "function", "fn", "lambda", 13 | "var", "let", "const", "def", 14 | "type", "struct", "interface", "class", 15 | "if", "else", "for", "of", "in", "while", "break", "continue", "goto", "end", 16 | "select", "switch", "case", 17 | "import", "export", "package", "from", 18 | "go", "async", "await", 19 | "raise", "throw", "try", "catch", "except", "finally", 20 | "string", "rune", "byte", "int", "float", 21 | } 22 | highlighting_special_words = []string{ 23 | "self", "this", "true", "false", "True", "False", "nil", "null", "None", 24 | } 25 | ) 26 | 27 | func highlighting_styles(b *Buffer) [][]tcell.Style { 28 | return style_maps[b] 29 | } 30 | 31 | func init_highlighting() { 32 | hook_buffer("modified", highlight_buffer) 33 | } 34 | 35 | func highlight_buffer(b *Buffer) { 36 | s := style("default") 37 | ss := style("special") 38 | sse := style("search") 39 | svi := style("visual") 40 | sts := style("text.string") 41 | stn := style("text.number") 42 | stc := style("text.comment") 43 | str := style("text.reserved") 44 | stsp := style("text.special") 45 | 46 | style_map := make([][]tcell.Style, len(b.Data)) 47 | in_string := rune(0) 48 | for l := range b.Data { 49 | in_line_comment := false 50 | word := "" 51 | style_map[l] = make([]tcell.Style, len(b.Data[l])+1) 52 | for c, char := range b.Data[l] { 53 | prev_char := rune(0) 54 | if c > 0 { 55 | prev_char = b.Data[l][c-1] 56 | } 57 | 58 | // for numbers 59 | passed_alpha := false 60 | if isAlpha(prev_char) { 61 | passed_alpha = true 62 | } 63 | // for special words 64 | if isWord(char) { 65 | word += string(char) 66 | } else { 67 | word = "" 68 | } 69 | 70 | if high_len := search_highlight(b, l, c); high_len > 0 { 71 | for i := 0; i < high_len; i++ { 72 | if style_map[l][c+i] == 0 { 73 | style_map[l][c+i] = sse 74 | } 75 | } 76 | } 77 | if visualHighlight(b, l, c) { 78 | style_map[l][c] = svi 79 | continue 80 | } 81 | if in_line_comment { 82 | style_map[l][c] = stc 83 | continue 84 | } 85 | if in_string > 0 && c-1 > 0 && b.Data[l][c-1] == '\\' && (c-2 < 0 || b.Data[l][c-2] != '\\') { 86 | style_map[l][c] = sts 87 | continue 88 | } 89 | if char == '/' && prev_char == '/' { 90 | in_line_comment = true 91 | style_map[l][c] = stc 92 | style_map[l][c-1] = stc 93 | } 94 | if char == '\'' || char == '"' || char == '`' { 95 | if in_string == char { 96 | in_string = rune(0) 97 | } else if in_string == rune(0) { 98 | in_string = char 99 | } 100 | style_map[l][c] = sts 101 | } 102 | if style_map[l][c] != 0 { 103 | continue 104 | } 105 | if in_string > 0 { 106 | style_map[l][c] = sts 107 | } else if listContainsString(highlighting_special_words, word) && c+1 < len(b.Data[l]) && !isWord(b.Data[l][c+1]) { 108 | for i := len(word) - 1; i >= 0; i-- { 109 | style_map[l][c-i] = stsp 110 | } 111 | } else if listContainsString(highlighting_reserved_words, word) && c+1 < len(b.Data[l]) && !isWord(b.Data[l][c+1]) { 112 | for i := len(word) - 1; i >= 0; i-- { 113 | style_map[l][c-i] = str 114 | } 115 | } else if !passed_alpha && isNum(char) { 116 | style_map[l][c] = stn 117 | } else if strings.ContainsRune(specialChars, b.Data[l][c]) { 118 | style_map[l][c] = ss 119 | } else { 120 | style_map[l][c] = s 121 | } 122 | } 123 | } 124 | 125 | style_maps[b] = style_map 126 | } 127 | -------------------------------------------------------------------------------- /ryy/hooks.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | var ( 4 | hooks_buffer map[string][]func(*Buffer) 5 | ) 6 | 7 | func hook_buffer(name string, f func(*Buffer)) { 8 | if _, ok := hooks_buffer[name]; !ok { 9 | hooks_buffer[name] = []func(*Buffer){} 10 | } 11 | hooks_buffer[name] = append(hooks_buffer[name], f) 12 | } 13 | 14 | func hook_trigger_buffer(name string, b *Buffer) { 15 | if hooks, ok := hooks_buffer[name]; ok { 16 | for _, f := range hooks { 17 | f(b) 18 | } 19 | } 20 | } 21 | 22 | func init_hooks() { 23 | hooks_buffer = map[string][]func(*Buffer){} 24 | 25 | hook_buffer("moved", func(b *Buffer) { 26 | if currentViewTree.Leaf.Buf == b { 27 | currentViewTree.Leaf.AdjustScroll(b.LastRenderWidth, b.LastRenderHeight) 28 | } 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /ryy/init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/gdamore/tcell" 7 | "github.com/gdamore/tcell/encoding" 8 | ) 9 | 10 | func initScreen() { 11 | var err error 12 | screen, err = tcell.NewScreen() 13 | fatalError(err) 14 | err = screen.Init() 15 | fatalError(err) 16 | 17 | encoding.Register() 18 | tcell.SetEncodingFallback(tcell.EncodingFallbackASCII) 19 | 20 | screen.SetStyle(tcell.StyleDefault) 21 | screen.Clear() 22 | 23 | editorWidth, editorHeight = screen.Size() 24 | } 25 | 26 | func initTermEvents() { 27 | go func() { 28 | for { 29 | if screen == nil { 30 | break 31 | } 32 | termEvents <- screen.PollEvent() 33 | } 34 | }() 35 | } 36 | 37 | func initBuffers() { 38 | for _, arg := range os.Args[1:] { 39 | openBufferFromFile(arg) 40 | } 41 | if len(buffers) == 0 { 42 | openBufferNamed("*scratch*") 43 | } 44 | } 45 | 46 | func initViews() { 47 | view := NewView(buffers[0]) 48 | rootViewTree = &ViewTree{Leaf: view} 49 | currentViewTree = rootViewTree 50 | } 51 | -------------------------------------------------------------------------------- /ryy/key.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | "unicode" 6 | 7 | "github.com/gdamore/tcell" 8 | ) 9 | 10 | type Key struct { 11 | Mod tcell.ModMask 12 | Key tcell.Key 13 | Chr rune 14 | } 15 | 16 | const ( 17 | KeyTypeCatchall tcell.Key = iota + 5000 18 | KeyTypeAlpha 19 | KeyTypeNum 20 | KeyTypeAlphaNum 21 | ) 22 | 23 | func NewKeyFromEvent(ev *tcell.EventKey) *Key { 24 | k, r, m := ev.Key(), ev.Rune(), ev.Modifiers() 25 | 26 | keyName := ev.Name() 27 | if strings.HasPrefix(keyName, "Ctrl+") { 28 | k = tcell.KeyRune 29 | r = unicode.ToLower([]rune(keyName[5:6])[0]) 30 | } 31 | 32 | // Handle Ctrl-h 33 | if k == tcell.KeyBackspace { 34 | m |= tcell.ModCtrl 35 | k = tcell.KeyRune 36 | r = 'h' 37 | } 38 | 39 | if k != tcell.KeyRune { 40 | r = 0 41 | } 42 | 43 | return &Key{Mod: ev.Modifiers(), Key: k, Chr: r} 44 | } 45 | 46 | func NewKey(rep string) *Key { 47 | if rep == "$any" { 48 | return &Key{Key: KeyTypeCatchall} 49 | } else if rep == "$num" { 50 | return &Key{Key: KeyTypeNum} 51 | } else if rep == "$alpha" { 52 | return &Key{Key: KeyTypeAlpha} 53 | } else if rep == "$alphanum" { 54 | return &Key{Key: KeyTypeAlphaNum} 55 | } 56 | 57 | parts := strings.Split(rep, "-") 58 | if rep == "-" { 59 | parts = []string{"-"} 60 | } 61 | 62 | // Modifiers 63 | modMask := tcell.ModNone 64 | for _, part := range parts[:len(parts)-1] { 65 | switch part { 66 | case "C": 67 | modMask |= tcell.ModCtrl 68 | case "S": 69 | modMask |= tcell.ModShift 70 | case "A": 71 | modMask |= tcell.ModAlt 72 | case "M": 73 | modMask |= tcell.ModMeta 74 | } 75 | } 76 | 77 | // Key 78 | var r rune = 0 79 | var k tcell.Key 80 | lastPart := parts[len(parts)-1] 81 | switch lastPart { 82 | case "DEL": 83 | k = tcell.KeyDelete 84 | case "BAK": 85 | k = tcell.KeyBackspace2 86 | case "RET": 87 | k = tcell.KeyEnter 88 | case "SPC": 89 | k = tcell.KeyRune 90 | r = ' ' 91 | case "ESC": 92 | k = tcell.KeyEscape 93 | case "TAB": 94 | k = tcell.KeyTab 95 | default: 96 | k = tcell.KeyRune 97 | r = []rune(lastPart)[0] 98 | } 99 | 100 | return &Key{Mod: modMask, Key: k, Chr: r} 101 | } 102 | 103 | func (k *Key) String() string { 104 | if k.Key == KeyTypeCatchall { 105 | return "$any" 106 | } else if k.Key == KeyTypeNum { 107 | return "$num" 108 | } else if k.Key == KeyTypeAlpha { 109 | return "$alpha" 110 | } else if k.Key == KeyTypeAlphaNum { 111 | return "$alphanum" 112 | } 113 | 114 | mods := []string{} 115 | if k.Mod&tcell.ModCtrl != 0 { 116 | mods = append(mods, "C") 117 | } 118 | if k.Mod&tcell.ModShift != 0 { 119 | mods = append(mods, "S") 120 | } 121 | if k.Mod&tcell.ModAlt != 0 { 122 | mods = append(mods, "A") 123 | } 124 | if k.Mod&tcell.ModMeta != 0 { 125 | mods = append(mods, "M") 126 | } 127 | 128 | name := string(k.Chr) 129 | switch k.Key { 130 | case tcell.KeyDelete: 131 | name = "DEL" 132 | case tcell.KeyBackspace2: 133 | name = "BAK" 134 | case tcell.KeyEnter: 135 | name = "RET" 136 | case tcell.KeyEscape: 137 | name = "ESC" 138 | case tcell.KeyTab: 139 | name = "TAB" 140 | } 141 | if k.Key == tcell.KeyRune && k.Chr == ' ' { 142 | name = "SPC" 143 | } 144 | 145 | return strings.Join(append(mods, name), "-") 146 | } 147 | 148 | func (k *Key) IsRune() bool { 149 | return k.Mod == 0 && k.Key == tcell.KeyRune 150 | } 151 | 152 | // TODO implement alphanum match 153 | func (k1 *Key) Matches(k2 *Key) bool { 154 | if k1.Key == KeyTypeCatchall || k2.Key == KeyTypeCatchall { 155 | return true 156 | } 157 | if k1.Key == KeyTypeAlpha && k2.IsRune() && isAlpha(k2.Chr) { 158 | return true 159 | } 160 | if k2.Key == KeyTypeAlpha && k1.IsRune() && isAlpha(k1.Chr) { 161 | return true 162 | } 163 | if k1.Key == KeyTypeNum && k2.IsRune() && isNum(k2.Chr) { 164 | return true 165 | } 166 | if k2.Key == KeyTypeNum && k1.IsRune() && isNum(k1.Chr) { 167 | return true 168 | } 169 | return k1.Mod == k2.Mod && k1.Key == k2.Key && k1.Chr == k2.Chr 170 | } 171 | 172 | type KeyList struct { 173 | keys []*Key 174 | } 175 | 176 | func NewKeyList(rep string) *KeyList { 177 | kl := &KeyList{[]*Key{}} 178 | parts := strings.Split(rep, " ") 179 | for _, part := range parts { 180 | if part != "" { 181 | kl.keys = append(kl.keys, NewKey(part)) 182 | } 183 | } 184 | return kl 185 | } 186 | 187 | var k = NewKeyList 188 | 189 | func (kl *KeyList) String() string { 190 | rep := []string{} 191 | for _, k := range kl.keys { 192 | rep = append(rep, k.String()) 193 | } 194 | return strings.Join(rep, " ") 195 | } 196 | 197 | func (kl *KeyList) AddKey(k *Key) { 198 | kl.keys = append(kl.keys, k) 199 | } 200 | 201 | func (kl1 *KeyList) Matches(kl2 *KeyList) bool { 202 | if len(kl1.keys) != len(kl2.keys) { 203 | return false 204 | } 205 | for i := range kl1.keys { 206 | if !kl1.keys[i].Matches(kl2.keys[i]) { 207 | return false 208 | } 209 | } 210 | return true 211 | } 212 | 213 | func (kl1 *KeyList) HasSuffix(kl2 *KeyList) *KeyList { 214 | for i := len(kl1.keys) - 1; i >= 0; i-- { 215 | tmp_kl := KeyList{kl1.keys[i:]} 216 | if tmp_kl.Matches(kl2) { 217 | return &tmp_kl 218 | } 219 | } 220 | return nil 221 | } 222 | -------------------------------------------------------------------------------- /ryy/mark.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type Mark struct { 4 | Loc *Location 5 | BufferName string 6 | } 7 | 8 | var marks = map[rune]*Mark{} 9 | 10 | func markCreate(markLetter rune, b *Buffer) *Mark { 11 | m := &Mark{Loc: b.Cursor.Clone(), BufferName: b.Name} 12 | marks[markLetter] = m 13 | return m 14 | } 15 | 16 | func getMark(markLetter rune) *Mark { 17 | return marks[markLetter] 18 | } 19 | 20 | func markJump(markLetter rune) { 21 | if m, ok := marks[markLetter]; ok { 22 | if b := showBuffer(m.BufferName); b != nil { 23 | b.MoveTo(m.Loc.Char, m.Loc.Line) 24 | } else { 25 | messageError("Can't find buffer named '" + m.BufferName + "'") 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ryy/mode_term.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func initTerm() { 4 | addMode("term") 5 | bind("term", k("ESC"), termExitMode) 6 | bind("term", k("$any"), termInput) 7 | } 8 | 9 | func termExitMode(vt *ViewTree, b *Buffer, kl *KeyList) { 10 | b.RemoveMode("term") 11 | } 12 | 13 | func termInput(vt *ViewTree, b *Buffer, kl *KeyList) { 14 | } 15 | -------------------------------------------------------------------------------- /ryy/mode_visual.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | func initVisual() { 4 | addMode("visual") 5 | bind("visual", k("ESC"), exitVisualMode) 6 | bind("visual", k("y"), visualModeYank) 7 | bind("visual", k("d"), visualModeDelete) 8 | bind("visual", k("p"), visualModePaste) 9 | bind("visual", k("c"), visualModeChange) 10 | 11 | addMode("visual-line") 12 | bind("visual-line", k("ESC"), exitVisualMode) 13 | bind("visual-line", k("y"), visualModeYank) 14 | bind("visual-line", k("d"), visualModeDelete) 15 | bind("visual-line", k("p"), visualModePaste) 16 | bind("visual-line", k("c"), visualModeChange) 17 | 18 | hook_buffer("moved", visualRehighlight) 19 | } 20 | 21 | // Run highlight when in visual mode and cursor mode as normal we 22 | // only recompute highlights when the buffer changes 23 | func visualRehighlight(b *Buffer) { 24 | if b.IsInMode("visual") || b.IsInMode("visual-line") { 25 | highlight_buffer(b) 26 | } 27 | } 28 | 29 | func visualHighlight(b *Buffer, l, c int) bool { 30 | in_visual_line := b.IsInMode("visual-line") 31 | if !b.IsInMode("visual") && !in_visual_line { 32 | return false 33 | } 34 | 35 | l1, l2 := orderLocations(b.Cursor, getMark('∫').Loc) 36 | 37 | if in_visual_line { 38 | // compare using line numbers 39 | return l1.Line <= l && l <= l2.Line 40 | } else { 41 | // compare using line numbers + char position 42 | loc := NewLocation(l, c) 43 | return loc.After(l1) && loc.Before(l2) || loc.Equal(l1) || loc.Equal(l2) 44 | } 45 | } 46 | 47 | func exitVisualMode(vt *ViewTree, b *Buffer, kl *KeyList) { 48 | b.RemoveMode("visual") 49 | b.RemoveMode("visual-line") 50 | highlight_buffer(b) 51 | } 52 | 53 | func enterVisualMode(vt *ViewTree, b *Buffer, kl *KeyList) { 54 | b.AddMode("visual") 55 | markCreate('∫', b) 56 | } 57 | 58 | func enterVisualBlockMode(vt *ViewTree, b *Buffer, kl *KeyList) { 59 | b.AddMode("visual-line") 60 | markCreate('∫', b) 61 | highlight_buffer(b) 62 | } 63 | 64 | func visualModeSelection(b *Buffer) ([]rune, *Location, *Location) { 65 | in_visual_line := b.IsInMode("visual-line") 66 | l1, l2 := orderLocations(b.Cursor, getMark('∫').Loc) 67 | data := []rune{} 68 | if in_visual_line { 69 | l1.Char = 0 70 | l2.Char = len(b.GetLine(l2.Line)) 71 | } 72 | for l := l1.Line; l <= l2.Line; l++ { 73 | start_char := 0 74 | if l == l1.Line { 75 | start_char = l1.Char 76 | } 77 | line_data := b.GetLine(l) 78 | end_char := len(line_data) 79 | if l == l2.Line { 80 | end_char = min(l2.Char+1, len(line_data)) 81 | } 82 | data = append(data, line_data[start_char:end_char]...) 83 | if l != l2.Line || end_char == len(line_data) { 84 | data = append(data, '\n') 85 | } 86 | } 87 | return data, l1, l2 88 | } 89 | 90 | func visualModeYank(vt *ViewTree, b *Buffer, kl *KeyList) { 91 | text, l1, _ := visualModeSelection(b) 92 | clipboardSet(defaultClipboard, text) 93 | 94 | b.MoveTo(l1.Char, l1.Line) 95 | exitVisualMode(vt, b, kl) 96 | } 97 | func visualModeDelete(vt *ViewTree, b *Buffer, kl *KeyList) { 98 | text, l1, _ := visualModeSelection(b) 99 | b.MoveTo(l1.Char, l1.Line) 100 | b.Remove(len(text)) 101 | 102 | exitVisualMode(vt, b, kl) 103 | } 104 | func visualModePaste(vt *ViewTree, b *Buffer, kl *KeyList) { 105 | text, l1, _ := visualModeSelection(b) 106 | clipboard_text := clipboardGet(defaultClipboard) 107 | b.MoveTo(l1.Char, l1.Line) 108 | b.Remove(len(text)) 109 | b.Insert(clipboard_text) 110 | clipboardSet(defaultClipboard, text) 111 | 112 | b.MoveTo(l1.Char, l1.Line) 113 | exitVisualMode(vt, b, kl) 114 | } 115 | func visualModeChange(vt *ViewTree, b *Buffer, kl *KeyList) { 116 | text, l1, _ := visualModeSelection(b) 117 | b.MoveTo(l1.Char, l1.Line) 118 | b.Remove(len(text)) 119 | 120 | exitVisualMode(vt, b, kl) 121 | enterInsertMode(vt, b, kl) 122 | } 123 | -------------------------------------------------------------------------------- /ryy/modes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "path/filepath" 6 | "strings" 7 | 8 | "github.com/gdamore/tcell" 9 | ) 10 | 11 | type CommandFn func(*ViewTree, *Buffer, *KeyList) 12 | 13 | type ModeBinding struct { 14 | k *KeyList 15 | f CommandFn 16 | } 17 | 18 | type Mode struct { 19 | name string 20 | bindings []*ModeBinding 21 | } 22 | 23 | var modes = map[string]*Mode{} 24 | 25 | func modeHandle(m *Mode, kl *KeyList) *KeyList { 26 | var match *KeyList = nil 27 | var matchBinding *ModeBinding = nil 28 | for _, binding := range m.bindings { 29 | if matched := kl.HasSuffix(binding.k); matched != nil { 30 | if match == nil || len(matched.keys) > len(match.keys) { 31 | matchBinding = binding 32 | match = matched 33 | } 34 | } 35 | } 36 | if match != nil { 37 | matchBinding.f(currentViewTree, currentViewTree.Leaf.Buf, match) 38 | return match 39 | } 40 | return nil 41 | } 42 | 43 | func findMode(name string) *Mode { 44 | if m, ok := modes[name]; ok { 45 | return m 46 | } else { 47 | return nil 48 | } 49 | } 50 | 51 | func mustFindMode(name string) *Mode { 52 | mode := findMode(name) 53 | if mode == nil { 54 | panic(fmt.Sprintf("no mode named '%s'", name)) 55 | } 56 | return mode 57 | } 58 | 59 | // Adds a new empty mode to the mode list, if not already present 60 | func addMode(name string) { 61 | if _, ok := modes[name]; !ok { 62 | modes[name] = &Mode{name: name, bindings: []*ModeBinding{}} 63 | } 64 | } 65 | 66 | func bind(mode_name string, k *KeyList, f CommandFn) { 67 | mode := mustFindMode(mode_name) 68 | 69 | // If this key is bound, update bound function 70 | for _, binding := range mode.bindings { 71 | if k.String() == binding.k.String() { 72 | binding.f = f 73 | } 74 | } 75 | // Else, it's a new binding, add it 76 | mode.bindings = append(mode.bindings, &ModeBinding{k: k, f: f}) 77 | } 78 | 79 | func initModes() { 80 | addMode("normal") 81 | bind("normal", k("m $alpha"), commandMark) 82 | bind("normal", k("' $alpha"), commandMoveToMark) 83 | bind("normal", k(":"), promptCommand) 84 | bind("normal", k("h"), moveLeft) 85 | bind("normal", k("j"), moveDown) 86 | bind("normal", k("k"), moveUp) 87 | bind("normal", k("l"), moveRight) 88 | bind("normal", k("0"), moveLineBeg) 89 | bind("normal", k("$"), moveLineEnd) 90 | bind("normal", k("g g"), moveTop) 91 | bind("normal", k("G"), moveBottom) 92 | bind("normal", k("C-u"), moveJumpUp) 93 | bind("normal", k("C-d"), moveJumpDown) 94 | bind("normal", k("z z"), moveCenterLine) 95 | bind("normal", k("w"), moveWordForward) 96 | bind("normal", k("e"), moveWordEndForward) 97 | bind("normal", k("b"), moveWordBackward) 98 | bind("normal", k("C-c"), cancelKeysEntered) 99 | bind("normal", k("C-g"), cancelKeysEntered) 100 | bind("normal", k("ESC ESC"), cancelKeysEntered) 101 | bind("normal", k("i"), enterInsertMode) 102 | bind("normal", k("a"), enterInsertModeAppend) 103 | bind("normal", k("A"), enterInsertModeEol) 104 | bind("normal", k("o"), enterInsertModeNl) 105 | bind("normal", k("O"), enterInsertModeNlUp) 106 | bind("normal", k("x"), removeChar) 107 | bind("normal", k("d d"), removeLine) 108 | bind("normal", k("y y"), commandCopyLine) 109 | bind("normal", k("p"), commandPaste) 110 | bind("normal", k("u"), commandUndo) 111 | bind("normal", k("C-r"), commandRedo) 112 | bind("normal", k("v"), enterVisualMode) 113 | bind("normal", k("V"), enterVisualBlockMode) 114 | 115 | addMode("insert") 116 | bind("insert", k("ESC"), enterNormalMode) 117 | bind("insert", k("C-c"), enterNormalMode) 118 | bind("insert", k("RET"), insertEnter) 119 | bind("insert", k("BAK"), insertBackspace) 120 | bind("insert", k("$any"), insert) 121 | 122 | addMode("prompt") 123 | bind("prompt", k("C-c"), promptCancel) 124 | bind("prompt", k("C-g"), promptCancel) 125 | bind("prompt", k("ESC"), promptCancel) 126 | bind("prompt", k("RET"), promptFinish) 127 | bind("prompt", k("BAK"), promptBackspace) 128 | bind("prompt", k("$any"), promptInsert) 129 | 130 | addMode("buffers") 131 | bind("buffers", k("q"), func(vt *ViewTree, b *Buffer, kl *KeyList) { 132 | closeCurrentBuffer(true) 133 | }) 134 | bind("buffers", k("RET"), func(vt *ViewTree, b *Buffer, kl *KeyList) { 135 | closeCurrentBuffer(true) 136 | showBuffer(string(b.Data[b.Cursor.Line])) 137 | }) 138 | 139 | addMode("directory") 140 | bind("directory", k("q"), func(vt *ViewTree, b *Buffer, kl *KeyList) { 141 | closeCurrentBuffer(true) 142 | }) 143 | bind("directory", k("RET"), func(vt *ViewTree, b *Buffer, kl *KeyList) { 144 | closeBuffer(currentViewTree.Leaf.Buf) 145 | selectAvailableBuffer(false) 146 | file_path := filepath.Join(b.Path, string(b.Data[b.Cursor.Line])) 147 | runCommand([]string{"edit", file_path}) 148 | }) 149 | 150 | // TODO Remove once we have user configurable bindings 151 | bind("normal", k("SPC b"), func(vt *ViewTree, b *Buffer, kl *KeyList) { 152 | runCommand([]string{"buffers"}) 153 | }) 154 | editCurrentFolder := func(vt *ViewTree, b *Buffer, kl *KeyList) { 155 | if b.Path == "" { 156 | runCommand([]string{"edit", "."}) 157 | } else { 158 | runCommand([]string{"edit", filepath.Dir(b.Path)}) 159 | } 160 | } 161 | bind("normal", k("SPC f"), editCurrentFolder) 162 | bind("normal", k("-"), editCurrentFolder) 163 | bind("normal", k("SPC n"), func(vt *ViewTree, b *Buffer, kl *KeyList) { 164 | runCommand([]string{"clearsearch"}) 165 | }) 166 | } 167 | 168 | func moveLeft(vt *ViewTree, b *Buffer, kl *KeyList) { 169 | b.Move(-1, 0) 170 | } 171 | func moveRight(vt *ViewTree, b *Buffer, kl *KeyList) { 172 | b.Move(1, 0) 173 | } 174 | func moveUp(vt *ViewTree, b *Buffer, kl *KeyList) { 175 | b.Move(0, -1) 176 | } 177 | func moveDown(vt *ViewTree, b *Buffer, kl *KeyList) { 178 | b.Move(0, 1) 179 | } 180 | func moveLineBeg(vt *ViewTree, b *Buffer, kl *KeyList) { 181 | b.MoveTo(0, b.Cursor.Line) 182 | } 183 | func moveLineEnd(vt *ViewTree, b *Buffer, kl *KeyList) { 184 | b.MoveTo(len(b.Data[b.Cursor.Line]), b.Cursor.Line) 185 | } 186 | func moveTop(vt *ViewTree, b *Buffer, kl *KeyList) { 187 | b.MoveTo(0, 0) 188 | } 189 | func moveBottom(vt *ViewTree, b *Buffer, kl *KeyList) { 190 | b.MoveTo(0, len(b.Data)-1) 191 | } 192 | func moveJumpUp(vt *ViewTree, b *Buffer, kl *KeyList) { 193 | b.Move(0, -15) 194 | } 195 | func moveJumpDown(vt *ViewTree, b *Buffer, kl *KeyList) { 196 | b.Move(0, 15) 197 | } 198 | func moveCenterLine(vt *ViewTree, b *Buffer, kl *KeyList) { 199 | vt.Leaf.CenterPending = true 200 | } 201 | func moveWordBackward(vt *ViewTree, b *Buffer, kl *KeyList) { 202 | b.MoveWordBackward() 203 | } 204 | func moveWordForward(vt *ViewTree, b *Buffer, kl *KeyList) { 205 | b.MoveWordForward() 206 | } 207 | func moveWordEndForward(vt *ViewTree, b *Buffer, kl *KeyList) { 208 | b.MoveWordEndForward() 209 | } 210 | 211 | func cancelKeysEntered(vt *ViewTree, b *Buffer, kl *KeyList) { 212 | keysEntered = k("") 213 | } 214 | 215 | // Enter in a new mode 216 | func enterMode(mode string) { 217 | editorMode = mode 218 | // TODO maybe not the best place to clear this 219 | message("") 220 | } 221 | 222 | func enterNormalMode(vt *ViewTree, b *Buffer, kl *KeyList) { 223 | moveLeft(vt, b, kl) 224 | enterMode("normal") 225 | } 226 | func enterInsertMode(vt *ViewTree, b *Buffer, kl *KeyList) { 227 | enterMode("insert") 228 | } 229 | func enterInsertModeAppend(vt *ViewTree, b *Buffer, kl *KeyList) { 230 | moveRight(vt, b, kl) 231 | enterMode("insert") 232 | } 233 | func enterInsertModeEol(vt *ViewTree, b *Buffer, kl *KeyList) { 234 | moveLineEnd(vt, b, kl) 235 | enterMode("insert") 236 | } 237 | func enterInsertModeNl(vt *ViewTree, b *Buffer, kl *KeyList) { 238 | moveLineEnd(vt, b, kl) 239 | b.Insert([]rune("\n")) 240 | b.Move(0, 1) // ensure a valid position 241 | enterMode("insert") 242 | } 243 | func enterInsertModeNlUp(vt *ViewTree, b *Buffer, kl *KeyList) { 244 | moveLineBeg(vt, b, kl) 245 | b.Insert([]rune("\n")) 246 | b.Move(0, 0) // ensure a valid position 247 | enterMode("insert") 248 | } 249 | 250 | func insertEnter(vt *ViewTree, b *Buffer, kl *KeyList) { 251 | i := 0 252 | for ; i < len(b.Data[b.Cursor.Line]) && isSpace(b.Data[b.Cursor.Line][i]); i++ { 253 | } 254 | b.Insert([]rune("\n" + strings.Repeat(" ", i))) 255 | b.MoveTo(i, b.Cursor.Line+1) 256 | } 257 | func insertBackspace(vt *ViewTree, b *Buffer, kl *KeyList) { 258 | if b.Cursor.Char == 0 { 259 | if b.Cursor.Line != 0 { 260 | moveUp(vt, b, kl) 261 | moveLineEnd(vt, b, kl) 262 | b.Remove(1) 263 | } 264 | } else { 265 | if b.CharAtLeft() != ' ' { 266 | b.Move(-1, 0) 267 | b.Remove(1) 268 | return 269 | } 270 | // handle spaces 271 | delete_n := 1 272 | if configGetBool("tab_to_spaces", b) { 273 | delete_n = int(configGetNumber("tab_width", b)) 274 | } 275 | for i := 0; i < delete_n && b.CharAtLeft() == ' '; i++ { 276 | b.Move(-1, 0) 277 | b.Remove(1) 278 | } 279 | } 280 | } 281 | func insert(vt *ViewTree, b *Buffer, kl *KeyList) { 282 | k := kl.keys[len(kl.keys)-1] 283 | if k.Key == tcell.KeyTab { 284 | if configGetBool("tab_to_spaces", b) { 285 | tabWidth := int(configGetNumber("tab_width", b)) 286 | b.Insert([]rune(strings.Repeat(" ", tabWidth))) 287 | b.Move(tabWidth, 0) 288 | } else { 289 | b.Insert([]rune{'\t'}) 290 | b.Move(1, 0) 291 | } 292 | } else if k.Key == tcell.KeyRune && k.Mod == 0 { 293 | b.Insert([]rune{k.Chr}) 294 | moveRight(vt, b, kl) 295 | } else { 296 | message("Can't insert '" + kl.String() + "'") 297 | } 298 | } 299 | 300 | func removeChar(vt *ViewTree, b *Buffer, kl *KeyList) { 301 | removed := b.Remove(1) 302 | clipboardSet(defaultClipboard, removed) 303 | } 304 | func removeLine(vt *ViewTree, b *Buffer, kl *KeyList) { 305 | moveLineBeg(vt, b, kl) 306 | removed := b.Remove(len(b.Data[b.Cursor.Line]) + 1) 307 | clipboardSet(defaultClipboard, removed) 308 | } 309 | 310 | func commandUndo(vt *ViewTree, b *Buffer, kl *KeyList) { 311 | b.Undo() 312 | } 313 | func commandRedo(vt *ViewTree, b *Buffer, kl *KeyList) { 314 | b.Redo() 315 | } 316 | func commandCopyLine(vt *ViewTree, b *Buffer, kl *KeyList) { 317 | value := make([]rune, len(b.Data[b.Cursor.Line])+1) 318 | copy(value, b.Data[b.Cursor.Line]) 319 | value[len(value)-1] = '\n' 320 | clipboardSet(defaultClipboard, value) 321 | } 322 | func commandPaste(vt *ViewTree, b *Buffer, kl *KeyList) { 323 | value := clipboardGet(defaultClipboard) 324 | if len(value) == 0 { 325 | message("Nothing to paste!") 326 | return 327 | } 328 | b.Move(1, 0) 329 | b.Insert(value) 330 | } 331 | 332 | func commandMark(vt *ViewTree, b *Buffer, kl *KeyList) { 333 | mark_letter := kl.keys[len(kl.keys)-1].Chr 334 | markCreate(mark_letter, b) 335 | } 336 | func commandMoveToMark(vt *ViewTree, b *Buffer, kl *KeyList) { 337 | mark_letter := kl.keys[len(kl.keys)-1].Chr 338 | markJump(mark_letter) 339 | } 340 | -------------------------------------------------------------------------------- /ryy/prompt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gdamore/tcell" 7 | ) 8 | 9 | var ( 10 | editorIsPromptActive = false 11 | editorPrompt = "" 12 | editorPromptValue = "" 13 | editorPromptCallbackFn func([]string) = nil 14 | editorPromptCompletionFn func(string) []string = nil 15 | ) 16 | 17 | func prompt(prompt string, compFn func(string) []string, cbFn func([]string)) { 18 | editorPrompt = prompt 19 | editorPromptValue = "" 20 | editorPromptCallbackFn = cbFn 21 | editorPromptCompletionFn = compFn 22 | enterMode("prompt") 23 | } 24 | 25 | func noopComplete(prefix string) []string { 26 | return []string{} 27 | } 28 | 29 | func promptUpdateCompletion() { 30 | // TODO 31 | } 32 | 33 | func promptCancel(vt *ViewTree, b *Buffer, kl *KeyList) { 34 | enterMode("normal") 35 | } 36 | 37 | func promptFinish(vt *ViewTree, b *Buffer, kl *KeyList) { 38 | enterMode("normal") 39 | // TODO better args parsing 40 | editorPromptCallbackFn(strings.Split(editorPromptValue, " ")) 41 | } 42 | 43 | func promptBackspace(vt *ViewTree, b *Buffer, kl *KeyList) { 44 | if len(editorPromptValue) > 0 { 45 | editorPromptValue = editorPromptValue[:len(editorPromptValue)-1] 46 | promptUpdateCompletion() 47 | } 48 | } 49 | 50 | func promptInsert(vt *ViewTree, b *Buffer, kl *KeyList) { 51 | k := kl.keys[len(kl.keys)-1] 52 | if k.Key == tcell.KeyRune && k.Mod == 0 { 53 | editorPromptValue += string(k.Chr) 54 | promptUpdateCompletion() 55 | } 56 | } 57 | 58 | func promptCommand(vt *ViewTree, b *Buffer, kl *KeyList) { 59 | prompt(":", func(prefix string) []string { 60 | // TODO provide command suggestions 61 | return []string{} 62 | }, runCommand) 63 | } 64 | -------------------------------------------------------------------------------- /ryy/render.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | func render() { 9 | width, height := editorWidth, editorHeight 10 | 11 | screen.Clear() 12 | 13 | renderViewTree(rootViewTree, 0, 0, width, height-1) 14 | 15 | renderMessageBar(width, height) 16 | 17 | screen.Show() 18 | } 19 | 20 | func renderMessageBar(width, height int) { 21 | s := style("default") 22 | 23 | if editorMode == "prompt" { 24 | p := editorPrompt + editorPromptValue 25 | write(s, 0, height-1, p) 26 | write(s.Reverse(true), len(p), height-1, " ") 27 | return 28 | } 29 | 30 | smb := s 31 | if editorMessageType == "error" { 32 | smb = style("message.error") 33 | } 34 | if editorMessage != "" { 35 | write(smb, 0, height-1, editorMessage) 36 | } else { 37 | write(smb, 0, height-1, keysEntered.String()) 38 | } 39 | lastKeyText := lastKey.String() 40 | write(s, width-len(lastKeyText)-1, height-1, lastKeyText) 41 | } 42 | 43 | func renderViewTree(vt *ViewTree, x, y, w, h int) { 44 | if vt.Leaf != nil { 45 | renderView(vt.Leaf, x, y, w, h) 46 | return 47 | } 48 | panic("unreachable") 49 | } 50 | 51 | func renderView(v *View, x, y, w, h int) { 52 | sc := style("cursor") 53 | sln := style("linenumber") 54 | ssb := style("statusbar") 55 | ssbh := style("statusbar.highlight") 56 | b := v.Buf 57 | 58 | b.LastRenderWidth = w 59 | b.LastRenderHeight = h 60 | 61 | styleMap := highlighting_styles(b) 62 | 63 | gutterw := len(strconv.Itoa(len(b.Data))) + 1 64 | sy := y 65 | line := v.LineOffset 66 | for line < len(b.Data) && sy < y+h-1 { 67 | write(sln, x, sy, padl(strconv.Itoa(line+1), gutterw-1, ' ')) 68 | 69 | sx := x + gutterw 70 | for c, char := range b.Data[line] { 71 | if v == currentViewTree.Leaf && line == b.Cursor.Line && c == b.Cursor.Char { 72 | sx += write(sc, sx, sy, string(char)) 73 | } else { 74 | sx += write(styleMap[line][c], sx, sy, string(char)) 75 | } 76 | if sx >= x+w { 77 | break 78 | } 79 | } 80 | if v == currentViewTree.Leaf && 81 | line == b.Cursor.Line && 82 | b.Cursor.Char == len(b.Data[b.Cursor.Line]) { 83 | write(sc, sx, sy, " ") 84 | } 85 | 86 | line++ 87 | sy++ 88 | } 89 | 90 | // Current mode 91 | modeStatus := editorMode 92 | for _, modeName := range b.Modes { 93 | modeStatus += "+" + modeName 94 | } 95 | modeStatus = " " + modeStatus + " " 96 | write(ssbh, x, y+h-1, modeStatus) 97 | 98 | // Position 99 | statusRight := fmt.Sprintf("(%d,%d) %d ", b.Cursor.Char+1, b.Cursor.Line+1, len(b.Data)) 100 | write(ssb, x+w-len(statusRight), y+h-1, statusRight) 101 | // File name 102 | statusLeft := " " + b.Name 103 | if b.Modified { 104 | statusLeft += " [+]" 105 | } 106 | write(ssb, x+len(modeStatus), y+h-1, padr(statusLeft, w-len(statusRight)-len(modeStatus), ' ')) 107 | } 108 | -------------------------------------------------------------------------------- /ryy/ry.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/gdamore/tcell" 8 | ) 9 | 10 | const ( 11 | specialChars = "[]{}()/\\" 12 | ) 13 | 14 | var ( 15 | keysEntered = NewKeyList("") 16 | lastKey = NewKeyList("") 17 | termEvents = make(chan tcell.Event, 500) 18 | defaultClipboard = '_' 19 | clipboards = map[rune][]rune{'_': []rune{}} 20 | editorMode = "normal" 21 | editorMessage = "" 22 | editorMessageType = "info" 23 | editorWidth = 0 24 | editorHeight = 0 25 | buffers = []*Buffer{} 26 | screen tcell.Screen = nil 27 | rootViewTree *ViewTree = nil 28 | currentViewTree *ViewTree = nil 29 | ) 30 | 31 | func main() { 32 | if len(os.Args) == 2 && os.Args[1] == "-v" { 33 | fmt.Println("ry v0.0.0") 34 | os.Exit(0) 35 | } 36 | 37 | defer handlePanics() 38 | 39 | initModes() 40 | initCommands() 41 | 42 | initConfig() 43 | init_hooks() 44 | init_highlighting() 45 | init_search() 46 | initVisual() 47 | initTerm() 48 | 49 | initScreen() 50 | initTermEvents() 51 | initBuffers() 52 | initViews() 53 | 54 | render() 55 | 56 | top: 57 | for { 58 | select { 59 | case ev := <-termEvents: 60 | switch ev := ev.(type) { 61 | case *tcell.EventKey: 62 | if ev.Key() == tcell.KeyCtrlQ { 63 | screen.Fini() 64 | screen = nil 65 | break top 66 | /* 67 | } else if ev.Key() == tcell.KeyEscape { 68 | kl := k("ESC") 69 | enter_normal_mode(current_view_tree, current_view_tree.leaf.buf, kl) 70 | last_key = kl 71 | keys_entered = k("") 72 | */ 73 | } else { 74 | keysEntered.AddKey(NewKeyFromEvent(ev)) 75 | 76 | buf := currentViewTree.Leaf.Buf 77 | for _, mode_name := range buf.Modes { 78 | if matched := modeHandle(mustFindMode(mode_name), keysEntered); matched != nil { 79 | keysEntered = k("") 80 | lastKey = matched 81 | continue top 82 | } 83 | } 84 | if matched := modeHandle(mustFindMode(editorMode), keysEntered); matched != nil { 85 | keysEntered = k("") 86 | lastKey = matched 87 | continue top 88 | } 89 | } 90 | case *tcell.EventResize: 91 | editorWidth, editorHeight = screen.Size() 92 | } 93 | default: 94 | render() 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ryy/ryy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Databingo/aih/31676d2ffa7fd0758f37d8e5b21ea464900fa215/ryy/ryy -------------------------------------------------------------------------------- /ryy/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Databingo/aih/31676d2ffa7fd0758f37d8e5b21ea464900fa215/ryy/screenshot.png -------------------------------------------------------------------------------- /ryy/search.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | last_search_buffer *Buffer = nil 10 | last_search = "" 11 | last_search_index = 0 12 | last_search_highlight = false 13 | last_search_results = []*Location{} 14 | ) 15 | 16 | func init_search() { 17 | bind("normal", k("/"), handle_search_start) 18 | bind("normal", k("N"), handle_search_prev) 19 | bind("normal", k("n"), handle_search_next) 20 | bind("normal", k("*"), handle_search_search_work_under_cursor) 21 | bind("normal", k("SPC n"), func(vt *ViewTree, b *Buffer, kl *KeyList) { 22 | search_clear() 23 | }) 24 | 25 | addCommand("clearsearch", func(args []string) { 26 | search_clear() 27 | }) 28 | addAlias("cs", "clearsearch") 29 | 30 | hook_buffer("modified", func(b *Buffer) { 31 | // Update search result indexes on buffer changes 32 | if last_search != "" && b == last_search_buffer { 33 | search_find_matches(b, last_search) 34 | last_search_index = len(last_search_results) - 1 35 | } 36 | }) 37 | } 38 | 39 | func search_clear() { 40 | last_search_highlight = false 41 | highlight_buffer(currentViewTree.Leaf.Buf) 42 | } 43 | 44 | func search_find_matches(b *Buffer, search string) { 45 | last_search = search 46 | re := regexp.MustCompile(regexp.QuoteMeta(search)) 47 | 48 | last_search_buffer = b 49 | last_search_results = []*Location{} 50 | for i, line := range b.Data { 51 | idxs := re.FindAllStringIndex(string(line), -1) 52 | for _, idx := range idxs { 53 | last_search_results = append(last_search_results, NewLocation(i, idx[0])) 54 | } 55 | } 56 | } 57 | 58 | func search_start(b *Buffer, search string) { 59 | if len(search) > 0 { 60 | search_find_matches(b, search) 61 | 62 | // TODO start index at first match after cursor 63 | last_search_index = len(last_search_results) - 1 64 | search_next(b) 65 | } 66 | } 67 | 68 | func handle_search_start(vt *ViewTree, b *Buffer, kl *KeyList) { 69 | prompt("/", noopComplete, func(args []string) { 70 | search_start(b, strings.Join(args, " ")) 71 | }) 72 | } 73 | 74 | func search_prev(b *Buffer) { 75 | if len(last_search_results) == 0 { 76 | message("No search result.") 77 | return 78 | } 79 | if last_search_index == 0 { 80 | last_search_index = len(last_search_results) - 1 81 | } else { 82 | last_search_index-- 83 | } 84 | last_search_highlight = true 85 | highlight_buffer(currentViewTree.Leaf.Buf) 86 | loc := last_search_results[last_search_index] 87 | b.MoveTo(loc.Char, loc.Line) 88 | 89 | } 90 | 91 | func handle_search_prev(vt *ViewTree, b *Buffer, kl *KeyList) { 92 | search_prev(b) 93 | } 94 | 95 | func search_next(b *Buffer) { 96 | if len(last_search_results) == 0 { 97 | message("No search result.") 98 | return 99 | } 100 | if last_search_index+1 == len(last_search_results) { 101 | last_search_index = 0 102 | } else { 103 | last_search_index++ 104 | } 105 | last_search_highlight = true 106 | highlight_buffer(currentViewTree.Leaf.Buf) 107 | loc := last_search_results[last_search_index] 108 | b.MoveTo(loc.Char, loc.Line) 109 | 110 | } 111 | 112 | func handle_search_next(vt *ViewTree, b *Buffer, kl *KeyList) { 113 | search_next(b) 114 | } 115 | 116 | func search_highlight(b *Buffer, l, c int) int { 117 | if !last_search_highlight { 118 | return 0 119 | } 120 | if last_search_buffer != b { 121 | return 0 122 | } 123 | for _, loc := range last_search_results { 124 | if l == loc.Line && c == loc.Char { 125 | return len(last_search) 126 | } 127 | } 128 | return 0 129 | } 130 | 131 | func handle_search_search_work_under_cursor(vt *ViewTree, b *Buffer, kl *KeyList) { 132 | search_start(b, string(b.WordUnderCursor())) 133 | } 134 | -------------------------------------------------------------------------------- /ryy/styles.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/gdamore/tcell" 4 | 5 | func style(name string) tcell.Style { 6 | // TODO make table based and configurable 7 | if name == "message.error" { 8 | return tcell.StyleDefault. 9 | Foreground(tcell.ColorMaroon) 10 | } 11 | if name == "statusbar" { 12 | return tcell.StyleDefault. 13 | Foreground(tcell.ColorWhite). 14 | Background(tcell.Color(5)) 15 | } 16 | if name == "statusbar.highlight" { 17 | return tcell.StyleDefault. 18 | Foreground(tcell.ColorWhite). 19 | Background(tcell.Color(6)) 20 | } 21 | if name == "linenumber" { 22 | return tcell.StyleDefault. 23 | Foreground(tcell.Color(6)) 24 | } 25 | if name == "search" { 26 | return tcell.StyleDefault. 27 | Foreground(tcell.ColorWhite). 28 | Background(tcell.ColorOlive) 29 | } 30 | if name == "visual" { 31 | return tcell.StyleDefault. 32 | Foreground(tcell.ColorWhite). 33 | Background(tcell.Color(0)) 34 | } 35 | if name == "special" { 36 | return tcell.StyleDefault. 37 | //Foreground(tcell.Color(0)) 38 | Foreground(tcell.Color(2)) 39 | } 40 | if name == "text.string" { 41 | return tcell.StyleDefault. 42 | Foreground(tcell.ColorNavy) 43 | } 44 | if name == "text.number" { 45 | return tcell.StyleDefault. 46 | //Foreground(tcell.ColorOlive) 47 | Foreground(tcell.Color(2)) 48 | } 49 | if name == "text.comment" { 50 | return tcell.StyleDefault. 51 | Foreground(10) 52 | } 53 | if name == "text.reserved" { 54 | return tcell.StyleDefault. 55 | Foreground(tcell.Color(8)) 56 | } 57 | if name == "text.special" { 58 | return tcell.StyleDefault. 59 | //Foreground(tcell.Color(6)) 60 | Foreground(tcell.Color(2)) 61 | } 62 | if name == "cursor" { 63 | return tcell.StyleDefault.Reverse(true) 64 | } 65 | return tcell.StyleDefault.Foreground(tcell.Color(2)) 66 | } 67 | -------------------------------------------------------------------------------- /ryy/terminal/.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | log 3 | boxterm 4 | -------------------------------------------------------------------------------- /ryy/terminal/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2013 James Gray 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without liitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and thismssion notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /ryy/terminal/README.md: -------------------------------------------------------------------------------- 1 | # terminal 2 | 3 | Package terminal is a vt10x terminal emulation backend, influenced 4 | largely by st, rxvt, xterm, and iTerm as reference. Use it for terminal 5 | muxing, a terminal emulation frontend, or wherever else you need 6 | terminal emulation. 7 | 8 | ## Installation 9 | 10 | go get j4k.co/terminal 11 | 12 | New to Go? Have a look at how [import paths work](http://golang.org/doc/code.html#remote). 13 | 14 | _Vendored at 0aabd5e5da824b33403ebde1df03776a0d45c7b2 on Nov 26th 2017._ 15 | -------------------------------------------------------------------------------- /ryy/terminal/color.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | // ANSI color values 4 | const ( 5 | Black Color = iota 6 | Red 7 | Green 8 | Yellow 9 | Blue 10 | Magenta 11 | Cyan 12 | LightGrey 13 | DarkGrey 14 | LightRed 15 | LightGreen 16 | LightYellow 17 | LightBlue 18 | LightMagenta 19 | LightCyan 20 | White 21 | ) 22 | 23 | // Default colors are potentially distinct to allow for special behavior. 24 | // For example, a transparent background. Otherwise, the simple case is to 25 | // map default colors to another color. 26 | const ( 27 | DefaultFG Color = 0x0 + iota 28 | DefaultBG 29 | ) 30 | 31 | // Color maps to the ANSI colors [0, 16) and the xterm colors [16, 256). 32 | type Color uint16 33 | 34 | // ANSI returns true if Color is within [0, 16). 35 | func (c Color) ANSI() bool { 36 | return (c < 16) 37 | } 38 | -------------------------------------------------------------------------------- /ryy/terminal/csi.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // CSI (Control Sequence Introducer) 9 | // ESC+[ 10 | type csiEscape struct { 11 | buf []byte 12 | args []int 13 | mode byte 14 | priv bool 15 | } 16 | 17 | func (c *csiEscape) reset() { 18 | c.buf = c.buf[:0] 19 | c.args = c.args[:0] 20 | c.mode = 0 21 | c.priv = false 22 | } 23 | 24 | func (c *csiEscape) put(b byte) bool { 25 | c.buf = append(c.buf, b) 26 | if b >= 0x40 && b <= 0x7E || len(c.buf) >= 256 { 27 | c.parse() 28 | return true 29 | } 30 | return false 31 | } 32 | 33 | func (c *csiEscape) parse() { 34 | c.mode = c.buf[len(c.buf)-1] 35 | if len(c.buf) == 1 { 36 | return 37 | } 38 | s := string(c.buf) 39 | c.args = c.args[:0] 40 | if s[0] == '?' { 41 | c.priv = true 42 | s = s[1:] 43 | } 44 | s = s[:len(s)-1] 45 | ss := strings.Split(s, ";") 46 | for _, p := range ss { 47 | i, err := strconv.Atoi(p) 48 | if err != nil { 49 | //t.logf("invalid CSI arg '%s'\n", p) 50 | break 51 | } 52 | c.args = append(c.args, i) 53 | } 54 | } 55 | 56 | func (c *csiEscape) arg(i, def int) int { 57 | if i >= len(c.args) || i < 0 { 58 | return def 59 | } 60 | return c.args[i] 61 | } 62 | 63 | // maxarg takes the maximum of arg(i, def) and def 64 | func (c *csiEscape) maxarg(i, def int) int { 65 | return max(c.arg(i, def), def) 66 | } 67 | 68 | func (t *State) handleCSI() { 69 | c := &t.csi 70 | switch c.mode { 71 | default: 72 | goto unknown 73 | case '@': // ICH - insert blank char 74 | t.insertBlanks(c.arg(0, 1)) 75 | case 'A': // CUU - cursor up 76 | t.moveTo(t.cur.x, t.cur.y-c.maxarg(0, 1)) 77 | case 'B', 'e': // CUD, VPR - cursor down 78 | t.moveTo(t.cur.x, t.cur.y+c.maxarg(0, 1)) 79 | case 'c': // DA - device attributes 80 | if c.arg(0, 0) == 0 { 81 | // TODO: write vt102 id 82 | } 83 | case 'C', 'a': // CUF, HPR - cursor forward 84 | t.moveTo(t.cur.x+c.maxarg(0, 1), t.cur.y) 85 | case 'D': // CUB - cursor backward 86 | t.moveTo(t.cur.x-c.maxarg(0, 1), t.cur.y) 87 | case 'E': // CNL - cursor down and first col 88 | t.moveTo(0, t.cur.y+c.arg(0, 1)) 89 | case 'F': // CPL - cursor up and first col 90 | t.moveTo(0, t.cur.y-c.arg(0, 1)) 91 | case 'g': // TBC - tabulation clear 92 | switch c.arg(0, 0) { 93 | // clear current tab stop 94 | case 0: 95 | t.tabs[t.cur.x] = false 96 | // clear all tabs 97 | case 3: 98 | for i := range t.tabs { 99 | t.tabs[i] = false 100 | } 101 | default: 102 | goto unknown 103 | } 104 | case 'G', '`': // CHA, HPA - Move to 105 | t.moveTo(c.arg(0, 1)-1, t.cur.y) 106 | case 'H', 'f': // CUP, HVP - move to 107 | t.moveAbsTo(c.arg(1, 1)-1, c.arg(0, 1)-1) 108 | case 'I': // CHT - cursor forward tabulation tab stops 109 | n := c.arg(0, 1) 110 | for i := 0; i < n; i++ { 111 | t.putTab(true) 112 | } 113 | case 'J': // ED - clear screen 114 | // TODO: sel.ob.x = -1 115 | switch c.arg(0, 0) { 116 | case 0: // below 117 | t.clear(t.cur.x, t.cur.y, t.cols-1, t.cur.y) 118 | if t.cur.y < t.rows-1 { 119 | t.clear(0, t.cur.y+1, t.cols-1, t.rows-1) 120 | } 121 | case 1: // above 122 | if t.cur.y > 1 { 123 | t.clear(0, 0, t.cols-1, t.cur.y-1) 124 | } 125 | t.clear(0, t.cur.y, t.cur.x, t.cur.y) 126 | case 2: // all 127 | t.clear(0, 0, t.cols-1, t.rows-1) 128 | default: 129 | goto unknown 130 | } 131 | case 'K': // EL - clear line 132 | switch c.arg(0, 0) { 133 | case 0: // right 134 | t.clear(t.cur.x, t.cur.y, t.cols-1, t.cur.y) 135 | case 1: // left 136 | t.clear(0, t.cur.y, t.cur.x, t.cur.y) 137 | case 2: // all 138 | t.clear(0, t.cur.y, t.cols-1, t.cur.y) 139 | } 140 | case 'S': // SU - scroll lines up 141 | t.scrollUp(t.top, c.arg(0, 1)) 142 | case 'T': // SD - scroll lines down 143 | t.scrollDown(t.top, c.arg(0, 1)) 144 | case 'L': // IL - insert blank lines 145 | t.insertBlankLines(c.arg(0, 1)) 146 | case 'l': // RM - reset mode 147 | t.setMode(c.priv, false, c.args) 148 | case 'M': // DL - delete lines 149 | t.deleteLines(c.arg(0, 1)) 150 | case 'X': // ECH - erase chars 151 | t.clear(t.cur.x, t.cur.y, t.cur.x+c.arg(0, 1)-1, t.cur.y) 152 | case 'P': // DCH - delete chars 153 | t.deleteChars(c.arg(0, 1)) 154 | case 'Z': // CBT - cursor backward tabulation tab stops 155 | n := c.arg(0, 1) 156 | for i := 0; i < n; i++ { 157 | t.putTab(false) 158 | } 159 | case 'd': // VPA - move to 160 | t.moveAbsTo(t.cur.x, c.arg(0, 1)-1) 161 | case 'h': // SM - set terminal mode 162 | t.setMode(c.priv, true, c.args) 163 | case 'm': // SGR - terminal attribute (color) 164 | t.setAttr(c.args) 165 | case 'r': // DECSTBM - set scrolling region 166 | if c.priv { 167 | goto unknown 168 | } else { 169 | t.setScroll(c.arg(0, 1)-1, c.arg(1, t.rows)-1) 170 | t.moveAbsTo(0, 0) 171 | } 172 | case 's': // DECSC - save cursor position (ANSI.SYS) 173 | t.saveCursor() 174 | case 'u': // DECRC - restore cursor position (ANSI.SYS) 175 | t.restoreCursor() 176 | } 177 | return 178 | unknown: // TODO: get rid of this goto 179 | t.logf("unknown CSI sequence '%c'\n", c.mode) 180 | // TODO: c.dump() 181 | } 182 | -------------------------------------------------------------------------------- /ryy/terminal/csi_test.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestCSIParse(t *testing.T) { 8 | var csi csiEscape 9 | csi.reset() 10 | csi.buf = []byte("s") 11 | csi.parse() 12 | if csi.mode != 's' || csi.arg(0, 17) != 17 || len(csi.args) != 0 { 13 | t.Fatal("CSI parse mismatch") 14 | } 15 | 16 | csi.reset() 17 | csi.buf = []byte("31T") 18 | csi.parse() 19 | if csi.mode != 'T' || csi.arg(0, 0) != 31 || len(csi.args) != 1 { 20 | t.Fatal("CSI parse mismatch") 21 | } 22 | 23 | csi.reset() 24 | csi.buf = []byte("48;2f") 25 | csi.parse() 26 | if csi.mode != 'f' || csi.arg(0, 0) != 48 || csi.arg(1, 0) != 2 || len(csi.args) != 2 { 27 | t.Fatal("CSI parse mismatch") 28 | } 29 | 30 | csi.reset() 31 | csi.buf = []byte("?25l") 32 | csi.parse() 33 | if csi.mode != 'l' || csi.arg(0, 0) != 25 || csi.priv != true || len(csi.args) != 1 { 34 | t.Fatal("CSI parse mismatch") 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /ryy/terminal/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package terminal is a vt10x terminal emulation backend, influenced 3 | largely by st, rxvt, xterm, and iTerm as reference. Use it for terminal 4 | muxing, a terminal emulation frontend, or wherever else you need 5 | terminal emulation. 6 | 7 | In development, but very usable. 8 | */ 9 | package terminal 10 | -------------------------------------------------------------------------------- /ryy/terminal/ioctl.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | "unsafe" 7 | ) 8 | 9 | func ioctl(f *os.File, cmd, p uintptr) error { 10 | _, _, errno := syscall.Syscall( 11 | syscall.SYS_IOCTL, 12 | f.Fd(), 13 | syscall.TIOCSWINSZ, 14 | p) 15 | if errno != 0 { 16 | return syscall.Errno(errno) 17 | } 18 | return nil 19 | } 20 | 21 | func (t *VT) ptyResize() error { 22 | if t.pty == nil { 23 | return nil 24 | } 25 | var w struct{ row, col, xpix, ypix uint16 } 26 | w.row = uint16(t.dest.rows) 27 | w.col = uint16(t.dest.cols) 28 | w.xpix = 16 * uint16(t.dest.cols) 29 | w.ypix = 16 * uint16(t.dest.rows) 30 | return ioctl(t.pty, syscall.TIOCSWINSZ, 31 | uintptr(unsafe.Pointer(&w))) 32 | } 33 | -------------------------------------------------------------------------------- /ryy/terminal/parse.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | func isControlCode(c rune) bool { 4 | return c < 0x20 || c == 0177 5 | } 6 | 7 | func (t *State) parse(c rune) { 8 | if isControlCode(c) { 9 | if t.handleControlCodes(c) || t.cur.attr.mode&attrGfx == 0 { 10 | return 11 | } 12 | } 13 | // TODO: update selection; see st.c:2450 14 | 15 | if t.mode&ModeWrap != 0 && t.cur.state&cursorWrapNext != 0 { 16 | t.lines[t.cur.y][t.cur.x].mode |= attrWrap 17 | t.newline(true) 18 | } 19 | 20 | if t.mode&ModeInsert != 0 && t.cur.x+1 < t.cols { 21 | // TODO: move shiz, look at st.c:2458 22 | t.logln("insert mode not implemented") 23 | } 24 | 25 | t.setChar(c, &t.cur.attr, t.cur.x, t.cur.y) 26 | if t.cur.x+1 < t.cols { 27 | t.moveTo(t.cur.x+1, t.cur.y) 28 | } else { 29 | t.cur.state |= cursorWrapNext 30 | } 31 | } 32 | 33 | func (t *State) parseEsc(c rune) { 34 | if t.handleControlCodes(c) { 35 | return 36 | } 37 | next := t.parse 38 | switch c { 39 | case '[': 40 | next = t.parseEscCSI 41 | case '#': 42 | next = t.parseEscTest 43 | case 'P', // DCS - Device Control String 44 | '_', // APC - Application Program Command 45 | '^', // PM - Privacy Message 46 | ']', // OSC - Operating System Command 47 | 'k': // old title set compatibility 48 | t.str.reset() 49 | t.str.typ = c 50 | next = t.parseEscStr 51 | case '(': // set primary charset G0 52 | next = t.parseEscAltCharset 53 | case ')', // set secondary charset G1 (ignored) 54 | '*', // set tertiary charset G2 (ignored) 55 | '+': // set quaternary charset G3 (ignored) 56 | case 'D': // IND - linefeed 57 | if t.cur.y == t.bottom { 58 | t.scrollUp(t.top, 1) 59 | } else { 60 | t.moveTo(t.cur.x, t.cur.y+1) 61 | } 62 | case 'E': // NEL - next line 63 | t.newline(true) 64 | case 'H': // HTS - horizontal tab stop 65 | t.tabs[t.cur.x] = true 66 | case 'M': // RI - reverse index 67 | if t.cur.y == t.top { 68 | t.scrollDown(t.top, 1) 69 | } else { 70 | t.moveTo(t.cur.x, t.cur.y-1) 71 | } 72 | case 'Z': // DECID - identify terminal 73 | // TODO: write to our writer our id 74 | case 'c': // RIS - reset to initial state 75 | t.reset() 76 | case '=': // DECPAM - application keypad 77 | t.mode |= ModeAppKeypad 78 | case '>': // DECPNM - normal keypad 79 | t.mode &^= ModeAppKeypad 80 | case '7': // DECSC - save cursor 81 | t.saveCursor() 82 | case '8': // DECRC - restore cursor 83 | t.restoreCursor() 84 | case '\\': // ST - stop 85 | default: 86 | t.logf("unknown ESC sequence '%c'\n", c) 87 | } 88 | t.state = next 89 | } 90 | 91 | func (t *State) parseEscCSI(c rune) { 92 | if t.handleControlCodes(c) { 93 | return 94 | } 95 | if t.csi.put(byte(c)) { 96 | t.state = t.parse 97 | t.handleCSI() 98 | } 99 | } 100 | 101 | func (t *State) parseEscStr(c rune) { 102 | switch c { 103 | case '\033': 104 | t.state = t.parseEscStrEnd 105 | case '\a': // backwards compatiblity to xterm 106 | t.state = t.parse 107 | t.handleSTR() 108 | default: 109 | t.str.put(c) 110 | } 111 | } 112 | 113 | func (t *State) parseEscStrEnd(c rune) { 114 | if t.handleControlCodes(c) { 115 | return 116 | } 117 | t.state = t.parse 118 | if c == '\\' { 119 | t.handleSTR() 120 | } 121 | } 122 | 123 | func (t *State) parseEscAltCharset(c rune) { 124 | if t.handleControlCodes(c) { 125 | return 126 | } 127 | switch c { 128 | case '0': // line drawing set 129 | t.cur.attr.mode |= attrGfx 130 | case 'B': // USASCII 131 | t.cur.attr.mode &^= attrGfx 132 | case 'A', // UK (ignored) 133 | '<', // multinational (ignored) 134 | '5', // Finnish (ignored) 135 | 'C', // Finnish (ignored) 136 | 'K': // German (ignored) 137 | default: 138 | t.logf("unknown alt. charset '%c'\n", c) 139 | } 140 | t.state = t.parse 141 | } 142 | 143 | func (t *State) parseEscTest(c rune) { 144 | if t.handleControlCodes(c) { 145 | return 146 | } 147 | // DEC screen alignment test 148 | if c == '8' { 149 | for y := 0; y < t.rows; y++ { 150 | for x := 0; x < t.cols; x++ { 151 | t.setChar('E', &t.cur.attr, x, y) 152 | } 153 | } 154 | } 155 | t.state = t.parse 156 | } 157 | 158 | func (t *State) handleControlCodes(c rune) bool { 159 | if !isControlCode(c) { 160 | return false 161 | } 162 | switch c { 163 | // HT 164 | case '\t': 165 | t.putTab(true) 166 | // BS 167 | case '\b': 168 | t.moveTo(t.cur.x-1, t.cur.y) 169 | // CR 170 | case '\r': 171 | t.moveTo(0, t.cur.y) 172 | // LF, VT, LF 173 | case '\f', '\v', '\n': 174 | // go to first col if mode is set 175 | t.newline(t.mode&ModeCRLF != 0) 176 | // BEL 177 | case '\a': 178 | // TODO: emit sound 179 | // TODO: window alert if not focused 180 | // ESC 181 | case 033: 182 | t.csi.reset() 183 | t.state = t.parseEsc 184 | // SO, SI 185 | case 016, 017: 186 | // different charsets not supported. apps should use the correct 187 | // alt charset escapes, probably for line drawing 188 | // SUB, CAN 189 | case 032, 030: 190 | t.csi.reset() 191 | // ignore ENQ, NUL, XON, XOFF, DEL 192 | case 005, 000, 021, 023, 0177: 193 | default: 194 | return false 195 | } 196 | return true 197 | } 198 | -------------------------------------------------------------------------------- /ryy/terminal/state.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | ) 7 | 8 | const ( 9 | tabspaces = 8 10 | ) 11 | 12 | const ( 13 | attrReverse = 1 << iota 14 | attrUnderline 15 | attrBold 16 | attrGfx 17 | attrItalic 18 | attrBlink 19 | attrWrap 20 | ) 21 | 22 | const ( 23 | cursorDefault = 1 << iota 24 | cursorWrapNext 25 | cursorOrigin 26 | ) 27 | 28 | // ModeFlag represents various terminal mode states. 29 | type ModeFlag uint32 30 | 31 | // Terminal modes 32 | const ( 33 | ModeWrap ModeFlag = 1 << iota 34 | ModeInsert 35 | ModeAppKeypad 36 | ModeAltScreen 37 | ModeCRLF 38 | ModeMouseButton 39 | ModeMouseMotion 40 | ModeReverse 41 | ModeKeyboardLock 42 | ModeHide 43 | ModeEcho 44 | ModeAppCursor 45 | ModeMouseSgr 46 | Mode8bit 47 | ModeBlink 48 | ModeFBlink 49 | ModeFocus 50 | ModeMouseX10 51 | ModeMouseMany 52 | ModeMouseMask = ModeMouseButton | ModeMouseMotion | ModeMouseX10 | ModeMouseMany 53 | ) 54 | 55 | // ChangeFlag represents possible state changes of the terminal. 56 | type ChangeFlag uint32 57 | 58 | // Terminal changes to occur in VT.ReadState 59 | const ( 60 | ChangedScreen ChangeFlag = 1 << iota 61 | ChangedTitle 62 | ) 63 | 64 | type glyph struct { 65 | c rune 66 | mode int16 67 | fg, bg Color 68 | } 69 | 70 | type line []glyph 71 | 72 | type cursor struct { 73 | attr glyph 74 | x, y int 75 | state uint8 76 | } 77 | 78 | type parseState func(c rune) 79 | 80 | // State represents the terminal emulation state. Use Lock/Unlock 81 | // methods to synchronize data access with VT. 82 | type State struct { 83 | DebugLogger *log.Logger 84 | 85 | mu sync.Mutex 86 | changed ChangeFlag 87 | cols, rows int 88 | lines []line 89 | altLines []line 90 | dirty []bool // line dirtiness 91 | anydirty bool 92 | cur, curSaved cursor 93 | top, bottom int // scroll limits 94 | mode ModeFlag 95 | state parseState 96 | str strEscape 97 | csi csiEscape 98 | numlock bool 99 | tabs []bool 100 | title string 101 | } 102 | 103 | func (t *State) logf(format string, args ...interface{}) { 104 | if t.DebugLogger != nil { 105 | t.DebugLogger.Printf(format, args...) 106 | } 107 | } 108 | 109 | func (t *State) logln(s string) { 110 | if t.DebugLogger != nil { 111 | t.DebugLogger.Println(s) 112 | } 113 | } 114 | 115 | func (t *State) lock() { 116 | t.mu.Lock() 117 | } 118 | 119 | func (t *State) unlock() { 120 | t.mu.Unlock() 121 | } 122 | 123 | // Lock locks the state object's mutex. 124 | func (t *State) Lock() { 125 | t.mu.Lock() 126 | } 127 | 128 | // Unlock resets change flags and unlocks the state object's mutex. 129 | func (t *State) Unlock() { 130 | t.resetChanges() 131 | t.mu.Unlock() 132 | } 133 | 134 | // Cell returns the character code, foreground color, and background 135 | // color at position (x, y) relative to the top left of the terminal. 136 | func (t *State) Cell(x, y int) (ch rune, fg Color, bg Color) { 137 | return t.lines[y][x].c, Color(t.lines[y][x].fg), Color(t.lines[y][x].bg) 138 | } 139 | 140 | // Cursor returns the current position of the cursor. 141 | func (t *State) Cursor() (int, int) { 142 | return t.cur.x, t.cur.y 143 | } 144 | 145 | // CursorVisible returns the visible state of the cursor. 146 | func (t *State) CursorVisible() bool { 147 | return t.mode&ModeHide == 0 148 | } 149 | 150 | // Mode tests if mode is currently set. 151 | func (t *State) Mode(mode ModeFlag) bool { 152 | return t.mode&mode != 0 153 | } 154 | 155 | // Title returns the current title set via the tty. 156 | func (t *State) Title() string { 157 | return t.title 158 | } 159 | 160 | /* 161 | // ChangeMask returns a bitfield of changes that have occured by VT. 162 | func (t *State) ChangeMask() ChangeFlag { 163 | return t.changed 164 | } 165 | */ 166 | 167 | // Changed returns true if change has occured. 168 | func (t *State) Changed(change ChangeFlag) bool { 169 | return t.changed&change != 0 170 | } 171 | 172 | // resetChanges resets the change mask and dirtiness. 173 | func (t *State) resetChanges() { 174 | for i := range t.dirty { 175 | t.dirty[i] = false 176 | } 177 | t.anydirty = false 178 | t.changed = 0 179 | } 180 | 181 | func (t *State) saveCursor() { 182 | t.curSaved = t.cur 183 | } 184 | 185 | func (t *State) restoreCursor() { 186 | t.cur = t.curSaved 187 | t.moveTo(t.cur.x, t.cur.y) 188 | } 189 | 190 | func (t *State) put(c rune) { 191 | t.state(c) 192 | } 193 | 194 | func (t *State) putTab(forward bool) { 195 | x := t.cur.x 196 | if forward { 197 | if x == t.cols { 198 | return 199 | } 200 | for x++; x < t.cols && !t.tabs[x]; x++ { 201 | } 202 | } else { 203 | if x == 0 { 204 | return 205 | } 206 | for x--; x > 0 && !t.tabs[x]; x-- { 207 | } 208 | } 209 | t.moveTo(x, t.cur.y) 210 | } 211 | 212 | func (t *State) newline(firstCol bool) { 213 | y := t.cur.y 214 | if y == t.bottom { 215 | cur := t.cur 216 | t.cur = t.defaultCursor() 217 | t.scrollUp(t.top, 1) 218 | t.cur = cur 219 | } else { 220 | y++ 221 | } 222 | if firstCol { 223 | t.moveTo(0, y) 224 | } else { 225 | t.moveTo(t.cur.x, y) 226 | } 227 | } 228 | 229 | // table from st, which in turn is from rxvt :) 230 | var gfxCharTable = [62]rune{ 231 | '↑', '↓', '→', '←', '█', '▚', '☃', // A - G 232 | 0, 0, 0, 0, 0, 0, 0, 0, // H - O 233 | 0, 0, 0, 0, 0, 0, 0, 0, // P - W 234 | 0, 0, 0, 0, 0, 0, 0, ' ', // X - _ 235 | '◆', '▒', '␉', '␌', '␍', '␊', '°', '±', // ` - g 236 | '␤', '␋', '┘', '┐', '┌', '└', '┼', '⎺', // h - o 237 | '⎻', '─', '⎼', '⎽', '├', '┤', '┴', '┬', // p - w 238 | '│', '≤', '≥', 'π', '≠', '£', '·', // x - ~ 239 | } 240 | 241 | func (t *State) setChar(c rune, attr *glyph, x, y int) { 242 | if attr.mode&attrGfx != 0 { 243 | if c >= 0x41 && c <= 0x7e && gfxCharTable[c-0x41] != 0 { 244 | c = gfxCharTable[c-0x41] 245 | } 246 | } 247 | t.changed |= ChangedScreen 248 | t.dirty[y] = true 249 | t.lines[y][x] = *attr 250 | t.lines[y][x].c = c 251 | //if t.options.BrightBold && attr.mode&attrBold != 0 && attr.fg < 8 { 252 | if attr.mode&attrBold != 0 && attr.fg < 8 { 253 | t.lines[y][x].fg = attr.fg + 8 254 | } 255 | if attr.mode&attrReverse != 0 { 256 | t.lines[y][x].fg = attr.bg 257 | t.lines[y][x].bg = attr.fg 258 | } 259 | } 260 | 261 | func (t *State) defaultCursor() cursor { 262 | c := cursor{} 263 | c.attr.fg = DefaultFG 264 | c.attr.bg = DefaultBG 265 | return c 266 | } 267 | 268 | func (t *State) reset() { 269 | t.cur = t.defaultCursor() 270 | t.saveCursor() 271 | for i := range t.tabs { 272 | t.tabs[i] = false 273 | } 274 | for i := tabspaces; i < len(t.tabs); i += tabspaces { 275 | t.tabs[i] = true 276 | } 277 | t.top = 0 278 | t.bottom = t.rows - 1 279 | t.mode = ModeWrap 280 | t.clear(0, 0, t.rows-1, t.cols-1) 281 | t.moveTo(0, 0) 282 | } 283 | 284 | // TODO: definitely can improve allocs 285 | func (t *State) resize(cols, rows int) bool { 286 | if cols == t.cols && rows == t.rows { 287 | return false 288 | } 289 | if cols < 1 || rows < 1 { 290 | return false 291 | } 292 | slide := t.cur.y - rows + 1 293 | if slide > 0 { 294 | copy(t.lines, t.lines[slide:slide+rows]) 295 | copy(t.altLines, t.altLines[slide:slide+rows]) 296 | } 297 | 298 | lines, altLines, tabs := t.lines, t.altLines, t.tabs 299 | t.lines = make([]line, rows) 300 | t.altLines = make([]line, rows) 301 | t.dirty = make([]bool, rows) 302 | t.tabs = make([]bool, cols) 303 | 304 | minrows := min(rows, t.rows) 305 | mincols := min(cols, t.cols) 306 | t.changed |= ChangedScreen 307 | for i := 0; i < rows; i++ { 308 | t.dirty[i] = true 309 | t.lines[i] = make(line, cols) 310 | t.altLines[i] = make(line, cols) 311 | } 312 | for i := 0; i < minrows; i++ { 313 | copy(t.lines[i], lines[i]) 314 | copy(t.altLines[i], altLines[i]) 315 | } 316 | copy(t.tabs, tabs) 317 | if cols > t.cols { 318 | i := t.cols - 1 319 | for i > 0 && !tabs[i] { 320 | i-- 321 | } 322 | for i += tabspaces; i < len(tabs); i += tabspaces { 323 | tabs[i] = true 324 | } 325 | } 326 | 327 | t.cols = cols 328 | t.rows = rows 329 | t.setScroll(0, rows-1) 330 | t.moveTo(t.cur.x, t.cur.y) 331 | for i := 0; i < 2; i++ { 332 | if mincols < cols && minrows > 0 { 333 | t.clear(mincols, 0, cols-1, minrows-1) 334 | } 335 | if cols > 0 && minrows < rows { 336 | t.clear(0, minrows, cols-1, rows-1) 337 | } 338 | t.swapScreen() 339 | } 340 | return slide > 0 341 | } 342 | 343 | func (t *State) clear(x0, y0, x1, y1 int) { 344 | if x0 > x1 { 345 | x0, x1 = x1, x0 346 | } 347 | if y0 > y1 { 348 | y0, y1 = y1, y0 349 | } 350 | x0 = clamp(x0, 0, t.cols-1) 351 | x1 = clamp(x1, 0, t.cols-1) 352 | y0 = clamp(y0, 0, t.rows-1) 353 | y1 = clamp(y1, 0, t.rows-1) 354 | t.changed |= ChangedScreen 355 | for y := y0; y <= y1; y++ { 356 | t.dirty[y] = true 357 | for x := x0; x <= x1; x++ { 358 | t.lines[y][x] = t.cur.attr 359 | t.lines[y][x].c = ' ' 360 | } 361 | } 362 | } 363 | 364 | func (t *State) clearAll() { 365 | t.clear(0, 0, t.cols-1, t.rows-1) 366 | } 367 | 368 | func (t *State) moveAbsTo(x, y int) { 369 | if t.cur.state&cursorOrigin != 0 { 370 | y += t.top 371 | } 372 | t.moveTo(x, y) 373 | } 374 | 375 | func (t *State) moveTo(x, y int) { 376 | var miny, maxy int 377 | if t.cur.state&cursorOrigin != 0 { 378 | miny = t.top 379 | maxy = t.bottom 380 | } else { 381 | miny = 0 382 | maxy = t.rows - 1 383 | } 384 | x = clamp(x, 0, t.cols-1) 385 | y = clamp(y, miny, maxy) 386 | t.changed |= ChangedScreen 387 | t.cur.state &^= cursorWrapNext 388 | t.cur.x = x 389 | t.cur.y = y 390 | } 391 | 392 | func (t *State) swapScreen() { 393 | t.lines, t.altLines = t.altLines, t.lines 394 | t.mode ^= ModeAltScreen 395 | t.dirtyAll() 396 | } 397 | 398 | func (t *State) dirtyAll() { 399 | t.changed |= ChangedScreen 400 | for y := 0; y < t.rows; y++ { 401 | t.dirty[y] = true 402 | } 403 | } 404 | 405 | func (t *State) setScroll(top, bottom int) { 406 | top = clamp(top, 0, t.rows-1) 407 | bottom = clamp(bottom, 0, t.rows-1) 408 | if top > bottom { 409 | top, bottom = bottom, top 410 | } 411 | t.top = top 412 | t.bottom = bottom 413 | } 414 | 415 | func min(a, b int) int { 416 | if a < b { 417 | return a 418 | } 419 | return b 420 | } 421 | 422 | func max(a, b int) int { 423 | if a > b { 424 | return a 425 | } 426 | return b 427 | } 428 | 429 | func clamp(val, min, max int) int { 430 | if val < min { 431 | return min 432 | } else if val > max { 433 | return max 434 | } 435 | return val 436 | } 437 | 438 | func between(val, min, max int) bool { 439 | if val < min || val > max { 440 | return false 441 | } 442 | return true 443 | } 444 | 445 | func (t *State) scrollDown(orig, n int) { 446 | n = clamp(n, 0, t.bottom-orig+1) 447 | t.clear(0, t.bottom-n+1, t.cols-1, t.bottom) 448 | t.changed |= ChangedScreen 449 | for i := t.bottom; i >= orig+n; i-- { 450 | t.lines[i], t.lines[i-n] = t.lines[i-n], t.lines[i] 451 | t.dirty[i] = true 452 | t.dirty[i-n] = true 453 | } 454 | 455 | // TODO: selection scroll 456 | } 457 | 458 | func (t *State) scrollUp(orig, n int) { 459 | n = clamp(n, 0, t.bottom-orig+1) 460 | t.clear(0, orig, t.cols-1, orig+n-1) 461 | t.changed |= ChangedScreen 462 | for i := orig; i <= t.bottom-n; i++ { 463 | t.lines[i], t.lines[i+n] = t.lines[i+n], t.lines[i] 464 | t.dirty[i] = true 465 | t.dirty[i+n] = true 466 | } 467 | 468 | // TODO: selection scroll 469 | } 470 | 471 | func (t *State) modMode(set bool, bit ModeFlag) { 472 | if set { 473 | t.mode |= bit 474 | } else { 475 | t.mode &^= bit 476 | } 477 | } 478 | 479 | func (t *State) setMode(priv bool, set bool, args []int) { 480 | if priv { 481 | for _, a := range args { 482 | switch a { 483 | case 1: // DECCKM - cursor key 484 | t.modMode(set, ModeAppCursor) 485 | case 5: // DECSCNM - reverse video 486 | mode := t.mode 487 | t.modMode(set, ModeReverse) 488 | if mode != t.mode { 489 | // TODO: redraw 490 | } 491 | case 6: // DECOM - origin 492 | if set { 493 | t.cur.state |= cursorOrigin 494 | } else { 495 | t.cur.state &^= cursorOrigin 496 | } 497 | t.moveAbsTo(0, 0) 498 | case 7: // DECAWM - auto wrap 499 | t.modMode(set, ModeWrap) 500 | // IGNORED: 501 | case 0, // error 502 | 2, // DECANM - ANSI/VT52 503 | 3, // DECCOLM - column 504 | 4, // DECSCLM - scroll 505 | 8, // DECARM - auto repeat 506 | 18, // DECPFF - printer feed 507 | 19, // DECPEX - printer extent 508 | 42, // DECNRCM - national characters 509 | 12: // att610 - start blinking cursor 510 | break 511 | case 25: // DECTCEM - text cursor enable mode 512 | t.modMode(!set, ModeHide) 513 | case 9: // X10 mouse compatibility mode 514 | t.modMode(false, ModeMouseMask) 515 | t.modMode(set, ModeMouseX10) 516 | case 1000: // report button press 517 | t.modMode(false, ModeMouseMask) 518 | t.modMode(set, ModeMouseButton) 519 | case 1002: // report motion on button press 520 | t.modMode(false, ModeMouseMask) 521 | t.modMode(set, ModeMouseMotion) 522 | case 1003: // enable all mouse motions 523 | t.modMode(false, ModeMouseMask) 524 | t.modMode(set, ModeMouseMany) 525 | case 1004: // send focus events to tty 526 | t.modMode(set, ModeFocus) 527 | case 1006: // extended reporting mode 528 | t.modMode(set, ModeMouseSgr) 529 | case 1034: 530 | t.modMode(set, Mode8bit) 531 | case 1049, // = 1047 and 1048 532 | 47, 1047: 533 | alt := t.mode&ModeAltScreen != 0 534 | if alt { 535 | t.clear(0, 0, t.cols-1, t.rows-1) 536 | } 537 | if !set || !alt { 538 | t.swapScreen() 539 | } 540 | if a != 1049 { 541 | break 542 | } 543 | fallthrough 544 | case 1048: 545 | if set { 546 | t.saveCursor() 547 | } else { 548 | t.restoreCursor() 549 | } 550 | case 1001: 551 | // mouse highlight mode; can hang the terminal by design when 552 | // implemented 553 | case 1005: 554 | // utf8 mouse mode; will confuse applications not supporting 555 | // utf8 and luit 556 | case 1015: 557 | // urxvt mangled mouse mode; incompatiblt and can be mistaken 558 | // for other control codes 559 | default: 560 | t.logf("unknown private set/reset mode %d\n", a) 561 | } 562 | } 563 | } else { 564 | for _, a := range args { 565 | switch a { 566 | case 0: // Error (ignored) 567 | case 2: // KAM - keyboard action 568 | t.modMode(set, ModeKeyboardLock) 569 | case 4: // IRM - insertion-replacement 570 | t.modMode(set, ModeInsert) 571 | t.logln("insert mode not implemented") 572 | case 12: // SRM - send/receive 573 | t.modMode(set, ModeEcho) 574 | case 20: // LNM - linefeed/newline 575 | t.modMode(set, ModeCRLF) 576 | case 34: 577 | t.logln("right-to-left mode not implemented") 578 | case 96: 579 | t.logln("right-to-left copy mode not implemented") 580 | default: 581 | t.logf("unknown set/reset mode %d\n", a) 582 | } 583 | } 584 | } 585 | } 586 | 587 | func (t *State) setAttr(attr []int) { 588 | if len(attr) == 0 { 589 | attr = []int{0} 590 | } 591 | for i := 0; i < len(attr); i++ { 592 | a := attr[i] 593 | switch a { 594 | case 0: 595 | t.cur.attr.mode &^= attrReverse | attrUnderline | attrBold | attrItalic | attrBlink 596 | t.cur.attr.fg = DefaultFG 597 | t.cur.attr.bg = DefaultBG 598 | case 1: 599 | t.cur.attr.mode |= attrBold 600 | case 3: 601 | t.cur.attr.mode |= attrItalic 602 | case 4: 603 | t.cur.attr.mode |= attrUnderline 604 | case 5, 6: // slow, rapid blink 605 | t.cur.attr.mode |= attrBlink 606 | case 7: 607 | t.cur.attr.mode |= attrReverse 608 | case 21, 22: 609 | t.cur.attr.mode &^= attrBold 610 | case 23: 611 | t.cur.attr.mode &^= attrItalic 612 | case 24: 613 | t.cur.attr.mode &^= attrUnderline 614 | case 25, 26: 615 | t.cur.attr.mode &^= attrBlink 616 | case 27: 617 | t.cur.attr.mode &^= attrReverse 618 | case 38: 619 | if i+2 < len(attr) && attr[i+1] == 5 { 620 | i += 2 621 | if between(attr[i], 0, 255) { 622 | t.cur.attr.fg = Color(attr[i]) 623 | } else { 624 | t.logf("bad fgcolor %d\n", attr[i]) 625 | } 626 | } else { 627 | t.logf("gfx attr %d unknown\n", a) 628 | } 629 | case 39: 630 | t.cur.attr.fg = DefaultFG 631 | case 48: 632 | if i+2 < len(attr) && attr[i+1] == 5 { 633 | i += 2 634 | if between(attr[i], 0, 255) { 635 | t.cur.attr.bg = Color(attr[i]) 636 | } else { 637 | t.logf("bad bgcolor %d\n", attr[i]) 638 | } 639 | } else { 640 | t.logf("gfx attr %d unknown\n", a) 641 | } 642 | case 49: 643 | t.cur.attr.bg = DefaultBG 644 | default: 645 | if between(a, 30, 37) { 646 | t.cur.attr.fg = Color(a - 30) 647 | } else if between(a, 40, 47) { 648 | t.cur.attr.bg = Color(a - 40) 649 | } else if between(a, 90, 97) { 650 | t.cur.attr.fg = Color(a - 90 + 8) 651 | } else if between(a, 100, 107) { 652 | t.cur.attr.bg = Color(a - 100 + 8) 653 | } else { 654 | t.logf("gfx attr %d unknown\n", a) 655 | } 656 | } 657 | } 658 | } 659 | 660 | func (t *State) insertBlanks(n int) { 661 | src := t.cur.x 662 | dst := src + n 663 | size := t.cols - dst 664 | t.changed |= ChangedScreen 665 | t.dirty[t.cur.y] = true 666 | 667 | if dst >= t.cols { 668 | t.clear(t.cur.x, t.cur.y, t.cols-1, t.cur.y) 669 | } else { 670 | copy(t.lines[t.cur.y][dst:dst+size], t.lines[t.cur.y][src:src+size]) 671 | t.clear(src, t.cur.y, dst-1, t.cur.y) 672 | } 673 | } 674 | 675 | func (t *State) insertBlankLines(n int) { 676 | if t.cur.y < t.top || t.cur.y > t.bottom { 677 | return 678 | } 679 | t.scrollDown(t.cur.y, n) 680 | } 681 | 682 | func (t *State) deleteLines(n int) { 683 | if t.cur.y < t.top || t.cur.y > t.bottom { 684 | return 685 | } 686 | t.scrollUp(t.cur.y, n) 687 | } 688 | 689 | func (t *State) deleteChars(n int) { 690 | src := t.cur.x + n 691 | dst := t.cur.x 692 | size := t.cols - src 693 | t.changed |= ChangedScreen 694 | t.dirty[t.cur.y] = true 695 | 696 | if src >= t.cols { 697 | t.clear(t.cur.x, t.cur.y, t.cols-1, t.cur.y) 698 | } else { 699 | copy(t.lines[t.cur.y][dst:dst+size], t.lines[t.cur.y][src:src+size]) 700 | t.clear(t.cols-n, t.cur.y, t.cols-1, t.cur.y) 701 | } 702 | } 703 | 704 | func (t *State) setTitle(title string) { 705 | t.changed |= ChangedTitle 706 | t.title = title 707 | } 708 | -------------------------------------------------------------------------------- /ryy/terminal/str.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | // STR sequences are similar to CSI sequences, but have string arguments (and 9 | // as far as I can tell, don't really have a name; STR is the name I took from 10 | // suckless which I imagine comes from rxvt or xterm). 11 | type strEscape struct { 12 | typ rune 13 | buf []rune 14 | args []string 15 | } 16 | 17 | func (s *strEscape) reset() { 18 | s.typ = 0 19 | s.buf = s.buf[:0] 20 | s.args = nil 21 | } 22 | 23 | func (s *strEscape) put(c rune) { 24 | // TODO: improve allocs with an array backed slice; bench first 25 | if len(s.buf) < 256 { 26 | s.buf = append(s.buf, c) 27 | } 28 | // Going by st, it is better to remain silent when the STR sequence is not 29 | // ended so that it is apparent to users something is wrong. The length sanity 30 | // check ensures we don't absorb the entire stream into memory. 31 | // TODO: see what rxvt or xterm does 32 | } 33 | 34 | func (s *strEscape) parse() { 35 | s.args = strings.Split(string(s.buf), ";") 36 | } 37 | 38 | func (s *strEscape) arg(i, def int) int { 39 | if i >= len(s.args) || i < 0 { 40 | return def 41 | } 42 | i, err := strconv.Atoi(s.args[i]) 43 | if err != nil { 44 | return def 45 | } 46 | return i 47 | } 48 | 49 | func (s *strEscape) argString(i int, def string) string { 50 | if i >= len(s.args) || i < 0 { 51 | return def 52 | } 53 | return s.args[i] 54 | } 55 | 56 | func (t *State) handleSTR() { 57 | s := &t.str 58 | s.parse() 59 | 60 | switch s.typ { 61 | case ']': // OSC - operating system command 62 | switch d := s.arg(0, 0); d { 63 | case 0, 1, 2: 64 | title := s.argString(1, "") 65 | if title != "" { 66 | t.setTitle(title) 67 | } 68 | case 4: // color set 69 | if len(s.args) < 3 { 70 | break 71 | } 72 | // setcolorname(s.arg(1, 0), s.argString(2, "")) 73 | case 104: // color reset 74 | // TODO: complain about invalid color, redraw, etc. 75 | // setcolorname(s.arg(1, 0), nil) 76 | default: 77 | t.logf("unknown OSC command %d\n", d) 78 | // TODO: s.dump() 79 | } 80 | case 'k': // old title set compatibility 81 | title := s.argString(0, "") 82 | if title != "" { 83 | t.setTitle(title) 84 | } 85 | default: 86 | // TODO: Ignore these codes instead of complain? 87 | // 'P': // DSC - device control string 88 | // '_': // APC - application program command 89 | // '^': // PM - privacy message 90 | 91 | t.logf("unhandled STR sequence '%c'\n", s.typ) 92 | // t.str.dump() 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /ryy/terminal/str_test.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestSTRParse(t *testing.T) { 8 | var str strEscape 9 | str.reset() 10 | str.buf = []rune("0;some text") 11 | str.parse() 12 | if str.arg(0, 17) != 0 || str.argString(1, "") != "some text" { 13 | t.Fatal("STR parse mismatch") 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /ryy/terminal/vt.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "github.com/kr/pty" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "unicode" 11 | "unicode/utf8" 12 | ) 13 | 14 | // VT represents the virtual terminal emulator. 15 | type VT struct { 16 | dest *State 17 | rc io.ReadCloser 18 | br *bufio.Reader 19 | pty *os.File 20 | } 21 | 22 | // Start initializes a virtual terminal emulator with the target state 23 | // and a new pty file by starting the *exec.Command. The returned 24 | // *os.File is the pty file. 25 | func Start(state *State, cmd *exec.Cmd) (*VT, *os.File, error) { 26 | var err error 27 | t := &VT{ 28 | dest: state, 29 | } 30 | t.pty, err = pty.Start(cmd) 31 | if err != nil { 32 | return nil, nil, err 33 | } 34 | t.rc = t.pty 35 | t.init() 36 | return t, t.pty, nil 37 | } 38 | 39 | // Create initializes a virtual terminal emulator with the target state 40 | // and io.ReadCloser input. 41 | func Create(state *State, rc io.ReadCloser) (*VT, error) { 42 | t := &VT{ 43 | dest: state, 44 | rc: rc, 45 | } 46 | t.init() 47 | return t, nil 48 | } 49 | 50 | func (t *VT) init() { 51 | t.br = bufio.NewReader(t.rc) 52 | t.dest.numlock = true 53 | t.dest.state = t.dest.parse 54 | t.dest.cur.attr.fg = DefaultFG 55 | t.dest.cur.attr.bg = DefaultBG 56 | t.Resize(80, 24) 57 | t.dest.reset() 58 | } 59 | 60 | // File returns the pty file. 61 | func (t *VT) File() *os.File { 62 | return t.pty 63 | } 64 | 65 | // Write parses input and writes terminal changes to state. 66 | func (t *VT) Write(p []byte) (int, error) { 67 | var written int 68 | r := bytes.NewReader(p) 69 | t.dest.lock() 70 | defer t.dest.unlock() 71 | for { 72 | c, sz, err := r.ReadRune() 73 | if err != nil { 74 | if err == io.EOF { 75 | break 76 | } 77 | return written, err 78 | } 79 | written += sz 80 | if c == unicode.ReplacementChar && sz == 1 { 81 | if r.Len() == 0 { 82 | // not enough bytes for a full rune 83 | return written - 1, nil 84 | } 85 | t.dest.logln("invalid utf8 sequence") 86 | continue 87 | } 88 | t.dest.put(c) 89 | } 90 | return written, nil 91 | } 92 | 93 | // Close closes the pty or io.ReadCloser. 94 | func (t *VT) Close() error { 95 | return t.rc.Close() 96 | } 97 | 98 | // Parse blocks on read on pty or io.ReadCloser, then parses sequences until 99 | // buffer empties. State is locked as soon as first rune is read, and unlocked 100 | // when buffer is empty. 101 | // TODO: add tests for expected blocking behavior 102 | func (t *VT) Parse() error { 103 | var locked bool 104 | defer func() { 105 | if locked { 106 | t.dest.unlock() 107 | } 108 | }() 109 | for { 110 | c, sz, err := t.br.ReadRune() 111 | if err != nil { 112 | return err 113 | } 114 | if c == unicode.ReplacementChar && sz == 1 { 115 | t.dest.logln("invalid utf8 sequence") 116 | break 117 | } 118 | if !locked { 119 | t.dest.lock() 120 | locked = true 121 | } 122 | 123 | // put rune for parsing and update state 124 | t.dest.put(c) 125 | 126 | // break if our buffer is empty, or if buffer contains an 127 | // incomplete rune. 128 | n := t.br.Buffered() 129 | if n == 0 || (n < 4 && !fullRuneBuffered(t.br)) { 130 | break 131 | } 132 | } 133 | return nil 134 | } 135 | 136 | func fullRuneBuffered(br *bufio.Reader) bool { 137 | n := br.Buffered() 138 | buf, err := br.Peek(n) 139 | if err != nil { 140 | return false 141 | } 142 | return utf8.FullRune(buf) 143 | } 144 | 145 | // Resize reports new size to pty and updates state. 146 | func (t *VT) Resize(cols, rows int) { 147 | t.dest.lock() 148 | defer t.dest.unlock() 149 | _ = t.dest.resize(cols, rows) 150 | t.ptyResize() 151 | } 152 | -------------------------------------------------------------------------------- /ryy/terminal/vt_test.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | "io" 5 | "strings" 6 | "testing" 7 | ) 8 | 9 | func extractStr(t *State, x0, x1, row int) string { 10 | var s []rune 11 | for i := x0; i <= x1; i++ { 12 | c, _, _ := t.Cell(i, row) 13 | s = append(s, c) 14 | } 15 | return string(s) 16 | } 17 | 18 | func TestPlainChars(t *testing.T) { 19 | var st State 20 | term, err := Create(&st, nil) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | expected := "Hello world!" 25 | _, err = term.Write([]byte(expected)) 26 | if err != nil && err != io.EOF { 27 | t.Fatal(err) 28 | } 29 | actual := extractStr(&st, 0, len(expected)-1, 0) 30 | if expected != actual { 31 | t.Fatal(actual) 32 | } 33 | } 34 | 35 | func TestNewline(t *testing.T) { 36 | var st State 37 | term, err := Create(&st, nil) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | expected := "Hello world!\n...and more." 42 | _, err = term.Write([]byte("\033[20h")) // set CRLF mode 43 | if err != nil && err != io.EOF { 44 | t.Fatal(err) 45 | } 46 | _, err = term.Write([]byte(expected)) 47 | if err != nil && err != io.EOF { 48 | t.Fatal(err) 49 | } 50 | 51 | split := strings.Split(expected, "\n") 52 | actual := extractStr(&st, 0, len(split[0])-1, 0) 53 | actual += "\n" 54 | actual += extractStr(&st, 0, len(split[1])-1, 1) 55 | if expected != actual { 56 | t.Fatal(actual) 57 | } 58 | 59 | // A newline with a color set should not make the next line that color, 60 | // which used to happen if it caused a scroll event. 61 | st.moveTo(0, st.rows-1) 62 | _, err = term.Write([]byte("\033[1;37m\n$ \033[m")) 63 | if err != nil && err != io.EOF { 64 | t.Fatal(err) 65 | } 66 | _, fg, bg := st.Cell(st.Cursor()) 67 | if fg != DefaultFG { 68 | t.Fatal(st.cur.x, st.cur.y, fg, bg) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ryy/utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "unicode" 8 | "unicode/utf8" 9 | 10 | "github.com/gdamore/tcell" 11 | "github.com/go-errors/errors" 12 | runewidth "github.com/mattn/go-runewidth" 13 | ) 14 | 15 | func fatalError(err error) { 16 | if err != nil { 17 | fatal(err.Error()) 18 | } 19 | } 20 | 21 | func fatal(message string) { 22 | if screen != nil { 23 | screen.Fini() 24 | screen = nil 25 | } 26 | fmt.Printf("%v\n", message) 27 | os.Exit(1) 28 | } 29 | 30 | func handlePanics() { 31 | if err := recover(); err != nil { 32 | fatal(fmt.Sprintf("ry fatal error:\n%v\n%s", err, errors.Wrap(err, 2).ErrorStack())) 33 | } 34 | } 35 | 36 | func write(style tcell.Style, x, y int, str string) int { 37 | s := screen 38 | i := 0 39 | var deferred []rune 40 | dwidth := 0 41 | for _, r := range str { 42 | // Handle tabs 43 | if r == '\t' { 44 | // TODO setting 45 | tabWidth := int(configGetNumber("tab_width", nil)) 46 | 47 | // Print first tab char 48 | s.SetContent(x+i, y, '>', nil, style.Foreground(tcell.ColorAqua)) 49 | i++ 50 | 51 | // Add space till we reach tab column or tabWidth 52 | for j := 0; j < tabWidth-1 || i%tabWidth == tabWidth-1; j++ { 53 | s.SetContent(x+i, y, ' ', nil, style) 54 | i++ 55 | } 56 | 57 | deferred = nil 58 | continue 59 | } 60 | 61 | switch runewidth.RuneWidth(r) { 62 | case 0: 63 | if len(deferred) == 0 { 64 | deferred = append(deferred, ' ') 65 | dwidth = 1 66 | } 67 | case 1: 68 | if len(deferred) != 0 { 69 | s.SetContent(x+i, y, deferred[0], deferred[1:], style) 70 | i += dwidth 71 | } 72 | deferred = nil 73 | dwidth = 1 74 | case 2: 75 | if len(deferred) != 0 { 76 | s.SetContent(x+i, y, deferred[0], deferred[1:], style) 77 | i += dwidth 78 | } 79 | deferred = nil 80 | dwidth = 2 81 | } 82 | deferred = append(deferred, r) 83 | } 84 | 85 | if len(deferred) != 0 { 86 | s.SetContent(x+i, y, deferred[0], deferred[1:], style) 87 | i += dwidth 88 | } 89 | 90 | // i is the real width of what we just outputed 91 | return i 92 | } 93 | 94 | func listContainsString(list []string, search string) bool { 95 | for _, item := range list { 96 | if item == search { 97 | return true 98 | } 99 | } 100 | return false 101 | } 102 | 103 | func padr(str string, length int, padding rune) string { 104 | for utf8.RuneCountInString(str) < length { 105 | str = str + string(padding) 106 | } 107 | return str 108 | } 109 | 110 | func padl(str string, length int, padding rune) string { 111 | for utf8.RuneCountInString(str) < length { 112 | str = string(padding) + str 113 | } 114 | return str 115 | } 116 | 117 | func min(a, b int) int { 118 | if a < b { 119 | return a 120 | } 121 | return b 122 | } 123 | 124 | func max(a, b int) int { 125 | if a > b { 126 | return a 127 | } 128 | return b 129 | } 130 | 131 | func isWord(r rune) bool { 132 | return unicode.IsLetter(r) || unicode.IsNumber(r) || strings.ContainsRune("_", r) 133 | } 134 | 135 | func isSpace(r rune) bool { 136 | return r == ' ' || r == '\t' || r == '\n' 137 | } 138 | 139 | func isAlpha(r rune) bool { 140 | return (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') 141 | } 142 | 143 | func isNum(r rune) bool { 144 | return r >= '0' && r <= '9' 145 | } 146 | -------------------------------------------------------------------------------- /ryy/view.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math" 5 | 6 | "github.com/gdamore/tcell" 7 | ) 8 | 9 | type ViewHighlight struct { 10 | Beg *Location 11 | End *Location 12 | Style tcell.Style 13 | } 14 | 15 | type View struct { 16 | Buf *Buffer 17 | LineOffset int 18 | CenterPending bool 19 | 20 | Highlights []*ViewHighlight 21 | } 22 | 23 | func NewView(buf *Buffer) *View { 24 | return &View{ 25 | Buf: buf, 26 | LineOffset: 0, 27 | CenterPending: false, 28 | Highlights: []*ViewHighlight{}, 29 | } 30 | } 31 | 32 | func (v *View) AdjustScroll(w, h int) { 33 | l := v.Buf.Cursor.Line 34 | if v.CenterPending { 35 | v.LineOffset = max(l-int(math.Floor(float64(h-1)/2)), 1) 36 | v.CenterPending = false 37 | return 38 | } 39 | // too low 40 | // (h-2) as height includes status bar and moving to 0 based 41 | if l > h-2+v.LineOffset { 42 | v.LineOffset = max(l-h+2, 0) 43 | } 44 | // too high 45 | if l < v.LineOffset { 46 | v.LineOffset = l 47 | } 48 | } 49 | 50 | // }}} 51 | 52 | // {{{ ViewTree 53 | type ViewTree struct { 54 | Parent *ViewTree 55 | Left *ViewTree 56 | Right *ViewTree 57 | Top *ViewTree 58 | Bottom *ViewTree 59 | Leaf *View 60 | Size int 61 | } 62 | 63 | func NewViewTreeLeaf(parent *ViewTree, v *View) *ViewTree { 64 | return &ViewTree{Parent: parent, Leaf: v, Size: 50} 65 | } 66 | 67 | // }}} 68 | 69 | // {{{ message 70 | func message(m string) { 71 | editorMessage = m 72 | editorMessageType = "info" 73 | } 74 | 75 | func messageError(m string) { 76 | editorMessage = m 77 | editorMessageType = "error" 78 | } 79 | -------------------------------------------------------------------------------- /vi.go: -------------------------------------------------------------------------------- 1 | // + build vi 2 | package main 3 | 4 | import _ "embed" 5 | 6 | //go:embed vi 7 | var vi []byte 8 | --------------------------------------------------------------------------------