pax_global_header00006660000000000000000000000064146055100770014517gustar00rootroot0000000000000052 comment=d9e5b07b619ce63db8d4c642156a8a342690666e golang-go.mau-mauview-0.2.1+git20240409+f020cbb/000077500000000000000000000000001460551007700203435ustar00rootroot00000000000000golang-go.mau-mauview-0.2.1+git20240409+f020cbb/.pre-commit-config.yaml000066400000000000000000000005561460551007700246320ustar00rootroot00000000000000repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.1.0 hooks: - id: trailing-whitespace exclude_types: [markdown] - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - repo: https://github.com/tekwizely/pre-commit-golang rev: v1.0.0-beta.5 hooks: - id: go-imports-repo golang-go.mau-mauview-0.2.1+git20240409+f020cbb/LICENSE000066400000000000000000000405251460551007700213560ustar00rootroot00000000000000Mozilla Public License Version 2.0 ================================== 1. Definitions -------------- 1.1. "Contributor" means each individual or legal entity that creates, contributes to the creation of, or owns Covered Software. 1.2. "Contributor Version" means the combination of the Contributions of others (if any) used by a Contributor and that particular Contributor's Contribution. 1.3. "Contribution" means Covered Software of a particular Contributor. 1.4. "Covered Software" means Source Code Form to which the initial Contributor has attached the notice in Exhibit A, the Executable Form of such Source Code Form, and Modifications of such Source Code Form, in each case including portions thereof. 1.5. "Incompatible With Secondary Licenses" means (a) that the initial Contributor has attached the notice described in Exhibit B to the Covered Software; or (b) that the Covered Software was made available under the terms of version 1.1 or earlier of the License, but not also under the terms of a Secondary License. 1.6. "Executable Form" means any form of the work other than Source Code Form. 1.7. "Larger Work" means a work that combines Covered Software with other material, in a separate file or files, that is not Covered Software. 1.8. "License" means this document. 1.9. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently, any and all of the rights conveyed by this License. 1.10. "Modifications" means any of the following: (a) any file in Source Code Form that results from an addition to, deletion from, or modification of the contents of Covered Software; or (b) any new file in Source Code Form that contains any Covered Software. 1.11. "Patent Claims" of a Contributor means any patent claim(s), including without limitation, method, process, and apparatus claims, in any patent Licensable by such Contributor that would be infringed, but for the grant of the License, by the making, using, selling, offering for sale, having made, import, or transfer of either its Contributions or its Contributor Version. 1.12. "Secondary License" means either the GNU General Public License, Version 2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General Public License, Version 3.0, or any later versions of those licenses. 1.13. "Source Code Form" means the form of the work preferred for making modifications. 1.14. "You" (or "Your") means an individual or a legal entity exercising rights under this License. For legal entities, "You" includes any entity that controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity. 2. License Grants and Conditions -------------------------------- 2.1. Grants Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license: (a) under intellectual property rights (other than patent or trademark) Licensable by such Contributor to use, reproduce, make available, modify, display, perform, distribute, and otherwise exploit its Contributions, either on an unmodified basis, with Modifications, or as part of a Larger Work; and (b) under Patent Claims of such Contributor to make, use, sell, offer for sale, have made, import, and otherwise transfer either its Contributions or its Contributor Version. 2.2. Effective Date The licenses granted in Section 2.1 with respect to any Contribution become effective for each Contribution on the date the Contributor first distributes such Contribution. 2.3. Limitations on Grant Scope The licenses granted in this Section 2 are the only rights granted under this License. No additional rights or licenses will be implied from the distribution or licensing of Covered Software under this License. Notwithstanding Section 2.1(b) above, no patent license is granted by a Contributor: (a) for any code that a Contributor has removed from Covered Software; or (b) for infringements caused by: (i) Your and any other third party's modifications of Covered Software, or (ii) the combination of its Contributions with other software (except as part of its Contributor Version); or (c) under Patent Claims infringed by Covered Software in the absence of its Contributions. This License does not grant any rights in the trademarks, service marks, or logos of any Contributor (except as may be necessary to comply with the notice requirements in Section 3.4). 2.4. Subsequent Licenses No Contributor makes additional grants as a result of Your choice to distribute the Covered Software under a subsequent version of this License (see Section 10.2) or under the terms of a Secondary License (if permitted under the terms of Section 3.3). 2.5. Representation Each Contributor represents that the Contributor believes its Contributions are its original creation(s) or it has sufficient rights to grant the rights to its Contributions conveyed by this License. 2.6. Fair Use This License is not intended to limit any rights You have under applicable copyright doctrines of fair use, fair dealing, or other equivalents. 2.7. Conditions Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in Section 2.1. 3. Responsibilities ------------------- 3.1. Distribution of Source Form All distribution of Covered Software in Source Code Form, including any Modifications that You create or to which You contribute, must be under the terms of this License. You must inform recipients that the Source Code Form of the Covered Software is governed by the terms of this License, and how they can obtain a copy of this License. You may not attempt to alter or restrict the recipients' rights in the Source Code Form. 3.2. Distribution of Executable Form If You distribute Covered Software in Executable Form then: (a) such Covered Software must also be made available in Source Code Form, as described in Section 3.1, and You must inform recipients of the Executable Form how they can obtain a copy of such Source Code Form by reasonable means in a timely manner, at a charge no more than the cost of distribution to the recipient; and (b) You may distribute such Executable Form under the terms of this License, or sublicense it under different terms, provided that the license for the Executable Form does not attempt to limit or alter the recipients' rights in the Source Code Form under this License. 3.3. Distribution of a Larger Work You may create and distribute a Larger Work under terms of Your choice, provided that You also comply with the requirements of this License for the Covered Software. If the Larger Work is a combination of Covered Software with a work governed by one or more Secondary Licenses, and the Covered Software is not Incompatible With Secondary Licenses, this License permits You to additionally distribute such Covered Software under the terms of such Secondary License(s), so that the recipient of the Larger Work may, at their option, further distribute the Covered Software under the terms of either this License or such Secondary License(s). 3.4. Notices You may not remove or alter the substance of any license notices (including copyright notices, patent notices, disclaimers of warranty, or limitations of liability) contained within the Source Code Form of the Covered Software, except that You may alter any license notices to the extent required to remedy known factual inaccuracies. 3.5. Application of Additional Terms You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, You may do so only on Your own behalf, and not on behalf of any Contributor. You must make it absolutely clear that any such warranty, support, indemnity, or liability obligation is offered by You alone, and You hereby agree to indemnify every Contributor for any liability incurred by such Contributor as a result of warranty, support, indemnity or liability terms You offer. You may include additional disclaimers of warranty and limitations of liability specific to any jurisdiction. 4. Inability to Comply Due to Statute or Regulation --------------------------------------------------- If it is impossible for You to comply with any of the terms of this License with respect to some or all of the Covered Software due to statute, judicial order, or regulation then You must: (a) comply with the terms of this License to the maximum extent possible; and (b) describe the limitations and the code they affect. Such description must be placed in a text file included with all distributions of the Covered Software under this License. Except to the extent prohibited by statute or regulation, such description must be sufficiently detailed for a recipient of ordinary skill to be able to understand it. 5. Termination -------------- 5.1. The rights granted under this License will terminate automatically if You fail to comply with any of its terms. However, if You become compliant, then the rights granted under this License from a particular Contributor are reinstated (a) provisionally, unless and until such Contributor explicitly and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor fails to notify You of the non-compliance by some reasonable means prior to 60 days after You have come back into compliance. Moreover, Your grants from a particular Contributor are reinstated on an ongoing basis if such Contributor notifies You of the non-compliance by some reasonable means, this is the first time You have received notice of non-compliance with this License from such Contributor, and You become compliant prior to 30 days after Your receipt of the notice. 5.2. If You initiate litigation against any entity by asserting a patent infringement claim (excluding declaratory judgment actions, counter-claims, and cross-claims) alleging that a Contributor Version directly or indirectly infringes any patent, then the rights granted to You by any and all Contributors for the Covered Software under Section 2.1 of this License shall terminate. 5.3. In the event of termination under Sections 5.1 or 5.2 above, all end user license agreements (excluding distributors and resellers) which have been validly granted by You or Your distributors under this License prior to termination shall survive termination. ************************************************************************ * * * 6. Disclaimer of Warranty * * ------------------------- * * * * Covered Software is provided under this License on an "as is" * * basis, without warranty of any kind, either expressed, implied, or * * statutory, including, without limitation, warranties that the * * Covered Software is free of defects, merchantable, fit for a * * particular purpose or non-infringing. The entire risk as to the * * quality and performance of the Covered Software is with You. * * Should any Covered Software prove defective in any respect, You * * (not any Contributor) assume the cost of any necessary servicing, * * repair, or correction. This disclaimer of warranty constitutes an * * essential part of this License. No use of any Covered Software is * * authorized under this License except under this disclaimer. * * * ************************************************************************ ************************************************************************ * * * 7. Limitation of Liability * * -------------------------- * * * * Under no circumstances and under no legal theory, whether tort * * (including negligence), contract, or otherwise, shall any * * Contributor, or anyone who distributes Covered Software as * * permitted above, be liable to You for any direct, indirect, * * special, incidental, or consequential damages of any character * * including, without limitation, damages for lost profits, loss of * * goodwill, work stoppage, computer failure or malfunction, or any * * and all other commercial damages or losses, even if such party * * shall have been informed of the possibility of such damages. This * * limitation of liability shall not apply to liability for death or * * personal injury resulting from such party's negligence to the * * extent applicable law prohibits such limitation. Some * * jurisdictions do not allow the exclusion or limitation of * * incidental or consequential damages, so this exclusion and * * limitation may not apply to You. * * * ************************************************************************ 8. Litigation ------------- Any litigation relating to this License may be brought only in the courts of a jurisdiction where the defendant maintains its principal place of business and such litigation shall be governed by laws of that jurisdiction, without reference to its conflict-of-law provisions. Nothing in this Section shall prevent a party's ability to bring cross-claims or counter-claims. 9. Miscellaneous ---------------- This License represents the complete agreement concerning the subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not be used to construe this License against a Contributor. 10. Versions of the License --------------------------- 10.1. New Versions Mozilla Foundation is the license steward. Except as provided in Section 10.3, no one other than the license steward has the right to modify or publish new versions of this License. Each version will be given a distinguishing version number. 10.2. Effect of New Versions You may distribute the Covered Software under the terms of the version of the License under which You originally received the Covered Software, or under the terms of any subsequent version published by the license steward. 10.3. Modified Versions If you create software not governed by this License, and you want to create a new license for such software, you may create and use a modified version of this License if you rename the license and remove any references to the name of the license steward (except to note that such modified license differs from this License). 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses If You choose to distribute Source Code Form that is Incompatible With Secondary Licenses under the terms of this version of the License, the notice described in Exhibit B of this License must be attached. Exhibit A - Source Code Form License Notice ------------------------------------------- This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. You may add additional accurate notices of copyright ownership. Exhibit B - "Incompatible With Secondary Licenses" Notice --------------------------------------------------------- This Source Code Form is "Incompatible With Secondary Licenses", as defined by the Mozilla Public License, v. 2.0. golang-go.mau-mauview-0.2.1+git20240409+f020cbb/README.md000066400000000000000000000000331460551007700216160ustar00rootroot00000000000000# mauview Work in progress golang-go.mau-mauview-0.2.1+git20240409+f020cbb/application.go000066400000000000000000000123151460551007700231770ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2022 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package mauview import ( "errors" "fmt" "os" "strings" "sync" "time" "go.mau.fi/tcell" ) type Component interface { Draw(screen Screen) OnKeyEvent(event KeyEvent) bool OnPasteEvent(event PasteEvent) bool OnMouseEvent(event MouseEvent) bool } type Focusable interface { Focus() Blur() } type FocusableComponent interface { Component Focusable } type Application struct { screenLock sync.RWMutex screen tcell.Screen prevMouseEvt *tcell.EventMouse root Component updates chan interface{} redrawTicker *time.Ticker stop chan struct{} waitForStop chan struct{} alwaysClear bool } const queueSize = 255 func NewApplication() *Application { return &Application{ prevMouseEvt: &tcell.EventMouse{}, updates: make(chan interface{}, queueSize), redrawTicker: time.NewTicker(1 * time.Minute), stop: make(chan struct{}, 1), alwaysClear: true, } } func newScreen(events chan tcell.Event) (tcell.Screen, error) { if screen, err := tcell.NewScreen(); err != nil { return nil, fmt.Errorf("failed to create screen: %w", err) } else if err = screen.Init(); err != nil { return nil, fmt.Errorf("failed to initialize screen: %w", err) } else { screen.EnableMouse() screen.EnablePaste() go screen.ChannelEvents(events, nil) return screen, nil } } func (app *Application) SetRedrawTicker(tick time.Duration) { app.redrawTicker.Stop() app.redrawTicker = time.NewTicker(tick) } func (app *Application) Start() error { if app.root == nil { return errors.New("root component not set") } events := make(chan tcell.Event, queueSize) screen, err := newScreen(events) if err != nil { return err } app.screenLock.Lock() app.screen = screen app.screenLock.Unlock() app.waitForStop = make(chan struct{}) defer func() { app.screenLock.Lock() app.screen = nil app.screenLock.Unlock() if screen != nil { screen.Fini() } close(app.waitForStop) }() var pasteBuffer strings.Builder var isPasting bool for { var redraw bool var clear bool select { case eventInterface := <-events: switch event := eventInterface.(type) { case *tcell.EventKey: if isPasting { switch event.Key() { case tcell.KeyRune: pasteBuffer.WriteRune(event.Rune()) case tcell.KeyEnter: pasteBuffer.WriteByte('\n') } } else { redraw = app.root.OnKeyEvent(event) } case *tcell.EventPaste: if event.Start() { isPasting = true pasteBuffer.Reset() } else { customEvt := customPasteEvent{event, pasteBuffer.String()} isPasting = false pasteBuffer.Reset() redraw = app.root.OnPasteEvent(customEvt) } case *tcell.EventMouse: onlyButtons := event.Buttons() < tcell.WheelUp hasMotion := onlyButtons && app.prevMouseEvt.Buttons() == event.Buttons() customEvt := customMouseEvent{event, hasMotion} app.prevMouseEvt = event redraw = app.root.OnMouseEvent(customEvt) case *tcell.EventResize: clear = true redraw = true } case <-app.redrawTicker.C: redraw = true case updaterInterface := <-app.updates: switch updater := updaterInterface.(type) { case redrawUpdate: redraw = true case setRootUpdate: app.root = updater.newRoot focusable, ok := app.root.(Focusable) if ok { focusable.Focus() } redraw = true clear = true case suspendUpdate: err = screen.Suspend() if err != nil { // This shouldn't fail panic(err) } updater.wait() err = screen.Resume() if err != nil { screen.Fini() fmt.Println("Failed to resume screen:", err) os.Exit(40) } redraw = true clear = true } case <-app.stop: return nil } select { case <-app.stop: return nil default: } if redraw { if clear || app.alwaysClear { screen.Clear() } screen.HideCursor() app.root.Draw(screen) screen.Show() } } } func (app *Application) Stop() { select { case app.stop <- struct{}{}: default: } <-app.waitForStop } func (app *Application) ForceStop() { if screen := app.screen; screen != nil { screen.Fini() } select { case app.stop <- struct{}{}: default: } } type suspendUpdate struct { wait func() } type redrawUpdate struct{} type setRootUpdate struct { newRoot Component } func (app *Application) Suspend(wait func()) { app.updates <- suspendUpdate{wait} } func (app *Application) Redraw() { app.updates <- redrawUpdate{} } func (app *Application) SetRoot(view Component) { app.screenLock.RLock() defer app.screenLock.RUnlock() if app.screen != nil { app.updates <- setRootUpdate{view} } else { app.root = view focusable, ok := app.root.(Focusable) if ok { focusable.Focus() } } } // Screen returns the main tcell screen currently used in the app. func (app *Application) Screen() tcell.Screen { app.screenLock.RLock() screen := app.screen app.screenLock.RUnlock() return screen } func (app *Application) SetAlwaysClear(always bool) { app.alwaysClear = always } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/borders.go000066400000000000000000000025411460551007700223340ustar00rootroot00000000000000// From https://github.com/rivo/tview/blob/master/borders.go // Copyright (c) 2018 Oliver Kuederle // MIT license package mauview // Borders defines various borders used when primitives are drawn. // These may be changed to accommodate a different look and feel. var Borders = struct { Horizontal rune Vertical rune TopLeft rune TopRight rune BottomLeft rune BottomRight rune LeftT rune RightT rune TopT rune BottomT rune Cross rune HorizontalFocus rune VerticalFocus rune TopLeftFocus rune TopRightFocus rune BottomLeftFocus rune BottomRightFocus rune }{ Horizontal: BoxDrawingsLightHorizontal, Vertical: BoxDrawingsLightVertical, TopLeft: BoxDrawingsLightDownAndRight, TopRight: BoxDrawingsLightDownAndLeft, BottomLeft: BoxDrawingsLightUpAndRight, BottomRight: BoxDrawingsLightUpAndLeft, LeftT: BoxDrawingsLightVerticalAndRight, RightT: BoxDrawingsLightVerticalAndLeft, TopT: BoxDrawingsLightDownAndHorizontal, BottomT: BoxDrawingsLightUpAndHorizontal, Cross: BoxDrawingsLightVerticalAndHorizontal, HorizontalFocus: BoxDrawingsDoubleHorizontal, VerticalFocus: BoxDrawingsDoubleVertical, TopLeftFocus: BoxDrawingsDoubleDownAndRight, TopRightFocus: BoxDrawingsDoubleDownAndLeft, BottomLeftFocus: BoxDrawingsDoubleUpAndRight, BottomRightFocus: BoxDrawingsDoubleUpAndLeft, } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/box.go000066400000000000000000000126271460551007700214720ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2019 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package mauview import ( "go.mau.fi/tcell" ) type KeyCaptureFunc func(event KeyEvent) KeyEvent type MouseCaptureFunc func(event MouseEvent) MouseEvent type PasteCaptureFunc func(event PasteEvent) PasteEvent type Box struct { border bool borderStyle tcell.Style backgroundColor *tcell.Color keyCapture KeyCaptureFunc mouseCapture MouseCaptureFunc pasteCapture PasteCaptureFunc focusCapture func() bool blurCapture func() bool title string inner Component innerScreen *ProxyScreen focused bool } func NewBox(inner Component) *Box { return &Box{ border: true, borderStyle: tcell.StyleDefault, backgroundColor: &Styles.PrimitiveBackgroundColor, inner: inner, innerScreen: &ProxyScreen{OffsetX: 1, OffsetY: 1}, } } func (box *Box) Focus() { box.focused = true if box.focusCapture != nil { if box.focusCapture() { return } } focusable, ok := box.inner.(Focusable) if ok { focusable.Focus() } } func (box *Box) Blur() { box.focused = false if box.blurCapture != nil { if box.blurCapture() { return } } focusable, ok := box.inner.(Focusable) if ok { focusable.Blur() } } func (box *Box) SetBorder(border bool) *Box { box.border = border if border { box.innerScreen.OffsetY = 1 box.innerScreen.OffsetX = 1 } else { box.innerScreen.OffsetY = 0 box.innerScreen.OffsetX = 0 } return box } func (box *Box) SetBorderStyle(borderStyle tcell.Style) *Box { box.borderStyle = borderStyle return box } func (box *Box) SetTitle(title string) *Box { box.title = title return box } func (box *Box) SetInnerComponent(component Component) *Box { box.inner = component return box } func (box *Box) SetMouseCaptureFunc(mouseCapture MouseCaptureFunc) *Box { box.mouseCapture = mouseCapture return box } func (box *Box) SetKeyCaptureFunc(keyCapture KeyCaptureFunc) *Box { box.keyCapture = keyCapture return box } func (box *Box) SetPasteCaptureFunc(pasteCapture PasteCaptureFunc) *Box { box.pasteCapture = pasteCapture return box } func (box *Box) SetFocusCaptureFunc(focusCapture func() bool) *Box { box.focusCapture = focusCapture return box } func (box *Box) SetBlurCaptureFunc(blurCapture func() bool) *Box { box.blurCapture = blurCapture return box } func (box *Box) SetBackgroundColor(color tcell.Color) *Box { box.backgroundColor = &color return box } func (box *Box) drawBorder(screen Screen) { width, height := screen.Size() var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune if box.focused { horizontal = Borders.HorizontalFocus vertical = Borders.VerticalFocus topLeft = Borders.TopLeftFocus topRight = Borders.TopRightFocus bottomLeft = Borders.BottomLeftFocus bottomRight = Borders.BottomRightFocus } else { horizontal = Borders.Horizontal vertical = Borders.Vertical topLeft = Borders.TopLeft topRight = Borders.TopRight bottomLeft = Borders.BottomLeft bottomRight = Borders.BottomRight } borderStyle := box.borderStyle if box.backgroundColor != nil { borderStyle = borderStyle.Background(*box.backgroundColor) } for x := 0; x < width; x++ { screen.SetContent(x, 0, horizontal, nil, borderStyle) screen.SetContent(x, height-1, horizontal, nil, borderStyle) } Print(screen, box.title, 1, 0, width-2, AlignCenter, Styles.BorderColor) for y := 0; y < height; y++ { screen.SetContent(0, y, vertical, nil, borderStyle) screen.SetContent(width-1, y, vertical, nil, borderStyle) } screen.SetContent(0, 0, topLeft, nil, borderStyle) screen.SetContent(width-1, 0, topRight, nil, borderStyle) screen.SetContent(0, height-1, bottomLeft, nil, borderStyle) screen.SetContent(width-1, height-1, bottomRight, nil, borderStyle) } func (box *Box) Draw(screen Screen) { width, height := screen.Size() border := false if box.backgroundColor != nil { screen.SetStyle(tcell.StyleDefault.Background(*box.backgroundColor)) screen.Clear() } if box.border && width >= 2 && height >= 2 { border = true box.drawBorder(screen) } if box.inner != nil { if border { box.innerScreen.Width = width - 2 box.innerScreen.Height = height - 2 } else { box.innerScreen.Width = width box.innerScreen.Height = height } box.innerScreen.Parent = screen box.inner.Draw(box.innerScreen) } } func (box *Box) OnKeyEvent(event KeyEvent) bool { if box.keyCapture != nil { event = box.keyCapture(event) if event == nil { return true } } if box.inner != nil { return box.inner.OnKeyEvent(event) } return false } func (box *Box) OnPasteEvent(event PasteEvent) bool { if box.pasteCapture != nil { event = box.pasteCapture(event) if event == nil { return true } } if box.inner != nil { return box.inner.OnPasteEvent(event) } return false } func (box *Box) OnMouseEvent(event MouseEvent) bool { if box.border { event = OffsetMouseEvent(event, -1, -1) } x, y := event.Position() if x < 0 || y < 0 || x > box.innerScreen.Width || y > box.innerScreen.Height { return false } if box.mouseCapture != nil { event = box.mouseCapture(event) if event == nil { return true } } if box.inner != nil { return box.inner.OnMouseEvent(event) } return false } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/button.go000066400000000000000000000046731460551007700222170ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2019 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package mauview import ( "go.mau.fi/tcell" ) type Button struct { text string style tcell.Style focusedStyle tcell.Style focused bool onClick func() } func NewButton(text string) *Button { return &Button{ text: text, style: tcell.StyleDefault.Background(Styles.ContrastBackgroundColor).Foreground(Styles.PrimaryTextColor), focusedStyle: tcell.StyleDefault.Background(Styles.MoreContrastBackgroundColor).Foreground(Styles.PrimaryTextColor), } } func (b *Button) SetText(text string) *Button { b.text = text return b } func (b *Button) SetForegroundColor(color tcell.Color) *Button { b.style = b.style.Foreground(color) return b } func (b *Button) SetBackgroundColor(color tcell.Color) *Button { b.style = b.style.Background(color) return b } func (b *Button) SetFocusedForegroundColor(color tcell.Color) *Button { b.focusedStyle = b.focusedStyle.Foreground(color) return b } func (b *Button) SetFocusedBackgroundColor(color tcell.Color) *Button { b.focusedStyle = b.focusedStyle.Background(color) return b } func (b *Button) SetStyle(style tcell.Style) *Button { b.style = style return b } func (b *Button) SetFocusedStyle(style tcell.Style) *Button { b.focusedStyle = style return b } func (b *Button) SetOnClick(fn func()) *Button { b.onClick = fn return b } func (b *Button) Focus() { b.focused = true } func (b *Button) Blur() { b.focused = false } func (b *Button) Draw(screen Screen) { width, _ := screen.Size() style := b.style if b.focused { style = b.focusedStyle } screen.SetStyle(style) screen.Clear() PrintWithStyle(screen, b.text, 0, 0, width, AlignCenter, style) } func (b *Button) Submit(event KeyEvent) bool { if b.onClick != nil { b.onClick() } return true } func (b *Button) OnKeyEvent(event KeyEvent) bool { if event.Key() == tcell.KeyEnter { if b.onClick != nil { b.onClick() } return true } return false } func (b *Button) OnMouseEvent(event MouseEvent) bool { if event.Buttons() == tcell.Button1 && !event.HasMotion() { if b.onClick != nil { b.onClick() } return true } return false } func (b *Button) OnPasteEvent(event PasteEvent) bool { return false } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/center.go000066400000000000000000000102701460551007700221520ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2019 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package mauview import ( "go.mau.fi/tcell" ) type FractionalCenterer struct { center *Centerer minWidth int minHeight int fractionWidth float64 fractionHeight float64 } func FractionalCenter(target Component, minWidth, minHeight int, fractionalWidth, fractionalHeight float64) *FractionalCenterer { return &FractionalCenterer{ center: Center(target, 0, 0), minWidth: minWidth, minHeight: minHeight, fractionWidth: fractionalWidth, fractionHeight: fractionalHeight, } } func (fc *FractionalCenterer) SetAlwaysFocusChild(always bool) *FractionalCenterer { fc.center.alwaysFocusChild = always return fc } func (fc *FractionalCenterer) Blur() { fc.center.Blur() } func (fc *FractionalCenterer) Focus() { fc.center.Focus() } func (fc *FractionalCenterer) OnMouseEvent(evt MouseEvent) bool { return fc.center.OnMouseEvent(evt) } func (fc *FractionalCenterer) OnKeyEvent(evt KeyEvent) bool { return fc.center.OnKeyEvent(evt) } func (fc *FractionalCenterer) OnPasteEvent(evt PasteEvent) bool { return fc.center.OnPasteEvent(evt) } func (fc *FractionalCenterer) Draw(screen Screen) { width, height := screen.Size() width = int(float64(width) * fc.fractionWidth) height = int(float64(height) * fc.fractionHeight) if width < fc.minWidth { width = fc.minWidth } if height < fc.minHeight { height = fc.minHeight } fc.center.SetSize(width, height) fc.center.Draw(screen) } type Centerer struct { target Component screen *ProxyScreen childFocused bool alwaysFocusChild bool } func Center(target Component, width, height int) *Centerer { return &Centerer{ target: target, screen: &ProxyScreen{Style: tcell.StyleDefault, Width: width, Height: height}, childFocused: false, alwaysFocusChild: false, } } func (center *Centerer) SetHeight(height int) { center.screen.Height = height } func (center *Centerer) SetWidth(width int) { center.screen.Width = width } func (center *Centerer) SetSize(width, height int) { center.screen.Width = width center.screen.Height = height } func (center *Centerer) SetAlwaysFocusChild(always bool) *Centerer { center.alwaysFocusChild = always return center } func (center *Centerer) Draw(screen Screen) { totalWidth, totalHeight := screen.Size() paddingX := (totalWidth - center.screen.Width) / 2 paddingY := (totalHeight - center.screen.Height) / 2 if paddingX >= 0 { center.screen.OffsetX = paddingX } if paddingY >= 0 { center.screen.OffsetY = paddingY } center.screen.Parent = screen center.target.Draw(center.screen) } func (center *Centerer) OnKeyEvent(evt KeyEvent) bool { return center.target.OnKeyEvent(evt) } func (center *Centerer) Focus() { if center.alwaysFocusChild { center.childFocused = true focusable, ok := center.target.(Focusable) if ok { focusable.Focus() } } } func (center *Centerer) Blur() { center.childFocused = false focusable, ok := center.target.(Focusable) if ok { focusable.Blur() } } func (center *Centerer) OnMouseEvent(evt MouseEvent) bool { x, y := evt.Position() x -= center.screen.OffsetX y -= center.screen.OffsetY focusable, ok := center.target.(Focusable) if x < 0 || y < 0 || x > center.screen.Width || y > center.screen.Height { if ok && evt.Buttons() == tcell.Button1 && !evt.HasMotion() { if center.alwaysFocusChild && !center.childFocused { focusable.Focus() center.childFocused = true } else if !center.alwaysFocusChild && center.childFocused { center.Blur() } return true } return false } focusChanged := false if ok && !center.childFocused && evt.Buttons() == tcell.Button1 && !evt.HasMotion() { focusable.Focus() center.childFocused = true focusChanged = true } return center.target.OnMouseEvent(OffsetMouseEvent(evt, -center.screen.OffsetX, -center.screen.OffsetY)) || focusChanged } func (center *Centerer) OnPasteEvent(evt PasteEvent) bool { return center.target.OnPasteEvent(evt) } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/eventhandler.go000066400000000000000000000054171460551007700233600ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2019 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package mauview import ( "go.mau.fi/tcell" ) // KeyEvent is an interface of the *tcell.EventKey type. type KeyEvent interface { tcell.Event // The rune corresponding to the key that was pressed. Rune() rune // The keyboard key that was pressed. Key() tcell.Key // The keyboard modifiers that were pressed during the event. Modifiers() tcell.ModMask } type customPasteEvent struct { *tcell.EventPaste text string } func (cpe customPasteEvent) Text() string { return cpe.text } // PasteEvent is an interface of the customPasteEvent type. type PasteEvent interface { tcell.Event // The text pasted. Text() string } type customMouseEvent struct { *tcell.EventMouse motion bool } func (cme customMouseEvent) HasMotion() bool { return cme.motion } // MouseEvent is an interface of the *tcell.EventMouse type. type MouseEvent interface { tcell.Event // The mouse buttons that were pressed. Buttons() tcell.ButtonMask // The keyboard modifiers that were pressed during the event. Modifiers() tcell.ModMask // The current position of the mouse. Position() (int, int) // Whether or not the event is a mouse move event. HasMotion() bool } // SimpleEventHandler is a simple implementation of the event handling methods required for components. type SimpleEventHandler struct { OnKey func(event KeyEvent) bool OnPaste func(event PasteEvent) bool OnMouse func(event MouseEvent) bool } func (seh *SimpleEventHandler) OnKeyEvent(event KeyEvent) bool { if seh.OnKey != nil { return seh.OnKey(event) } return false } func (seh *SimpleEventHandler) OnPasteEvent(event PasteEvent) bool { if seh.OnPaste != nil { return seh.OnPaste(event) } return false } func (seh *SimpleEventHandler) OnMouseEvent(event MouseEvent) bool { if seh.OnMouse != nil { return seh.OnMouse(event) } return false } type NoopEventHandler struct{} func (neh NoopEventHandler) OnKeyEvent(event KeyEvent) bool { return false } func (neh NoopEventHandler) OnPasteEvent(event PasteEvent) bool { return false } func (neh NoopEventHandler) OnMouseEvent(event MouseEvent) bool { return false } type proxyEventMouse struct { MouseEvent x int y int } func (evt *proxyEventMouse) Position() (int, int) { return evt.x, evt.y } // OffsetMouseEvent creates a new MouseEvent with the given offset. func OffsetMouseEvent(evt MouseEvent, offsetX, offsetY int) *proxyEventMouse { x, y := evt.Position() proxy, ok := evt.(*proxyEventMouse) if ok { evt = proxy.MouseEvent } return &proxyEventMouse{evt, x + offsetX, y + offsetY} } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/flex.go000066400000000000000000000074541460551007700216420ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2019 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package mauview import ( "go.mau.fi/tcell" ) type FlexDirection int const ( FlexRow FlexDirection = iota FlexColumn ) type flexChild struct { genericChild size int } type Flex struct { direction FlexDirection children []flexChild focused *flexChild } func NewFlex() *Flex { return &Flex{ children: []flexChild{}, focused: nil, direction: FlexColumn, } } func (flex *Flex) SetDirection(direction FlexDirection) *Flex { flex.direction = direction return flex } func (flex *Flex) AddFixedComponent(comp Component, size int) *Flex { flex.AddProportionalComponent(comp, -size) return flex } func (flex *Flex) AddProportionalComponent(comp Component, size int) *Flex { flex.children = append(flex.children, flexChild{ genericChild: genericChild{ target: comp, screen: &ProxyScreen{Style: tcell.StyleDefault}, }, size: -size, }) return flex } func (flex *Flex) RemoveComponent(comp Component) *Flex { for index := len(flex.children) - 1; index >= 0; index-- { if flex.children[index].target == comp { flex.children = append(flex.children[:index], flex.children[index+1:]...) } } return flex } func (flex *Flex) Draw(screen Screen) { width, height := screen.Size() screen.Clear() relTotalSize := width if flex.direction == FlexRow { relTotalSize = height } relParts := 0 for _, child := range flex.children { if child.size > 0 { relTotalSize -= child.size } else { relParts -= child.size } } offset := 0 for _, child := range flex.children { child.screen.Parent = screen size := child.size if size < 0 { size = relTotalSize * (-size) / relParts } if flex.direction == FlexRow { child.screen.Height = size child.screen.Width = width child.screen.OffsetY = offset child.screen.OffsetX = 0 } else { child.screen.Height = height child.screen.Width = size child.screen.OffsetY = 0 child.screen.OffsetX = offset } offset += size if flex.focused == nil || child != *flex.focused { child.target.Draw(child.screen) } } if flex.focused != nil { flex.focused.target.Draw(flex.focused.screen) } } func (flex *Flex) OnKeyEvent(event KeyEvent) bool { if flex.focused != nil { return flex.focused.target.OnKeyEvent(event) } return false } func (flex *Flex) OnPasteEvent(event PasteEvent) bool { if flex.focused != nil { return flex.focused.target.OnPasteEvent(event) } return false } func (flex *Flex) SetFocused(comp Component) { for _, child := range flex.children { if child.target == comp { flex.focused = &child flex.focused.Focus() } } } func (flex *Flex) OnMouseEvent(event MouseEvent) bool { if flex.focused != nil && flex.focused.screen.IsInArea(event.Position()) { screen := flex.focused.screen return flex.focused.target.OnMouseEvent(OffsetMouseEvent(event, -screen.OffsetX, -screen.OffsetY)) } for _, child := range flex.children { if child.screen.IsInArea(event.Position()) { focusChanged := false if event.Buttons() == tcell.Button1 && !event.HasMotion() { if flex.focused != nil { flex.focused.Blur() } flex.focused = &child flex.focused.Focus() focusChanged = true } return child.target.OnMouseEvent(OffsetMouseEvent(event, -child.screen.OffsetX, -child.screen.OffsetY)) || focusChanged } } if event.Buttons() == tcell.Button1 && flex.focused != nil && !event.HasMotion() { flex.focused.Blur() flex.focused = nil return true } return false } func (flex *Flex) Focus() {} func (flex *Flex) Blur() { if flex.focused != nil { flex.focused.Blur() flex.focused = nil } } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/form.go000066400000000000000000000036531460551007700216440ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2019 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package mauview import ( "go.mau.fi/tcell" ) type Form struct { *Grid items []*gridChild } type FormItem interface { Component Submit(event KeyEvent) bool } func NewForm() *Form { return &Form{ Grid: NewGrid(), } } func (form *Form) Draw(screen Screen) { form.Grid.Draw(screen) } func (form *Form) FocusNextItem() { for i := 0; i < len(form.items)-1; i++ { if form.focused == form.items[i] { form.setFocused(form.items[i+1]) return } } form.setFocused(form.items[0]) } func (form *Form) FocusPreviousItem() { for i := len(form.items) - 1; i > 0; i-- { if form.focused == form.items[i] { form.setFocused(form.items[i-1]) return } } form.setFocused(form.items[len(form.items)-1]) } func (form *Form) AddFormItem(comp Component, x, y, width, height int) *Form { child := form.Grid.createChild(comp, x, y, width, height) form.items = append(form.items, child) form.Grid.addChild(child) return form } func (form *Form) RemoveFormItem(comp Component) *Form { for index := len(form.items) - 1; index >= 0; index-- { if form.items[index].target == comp { form.items = append(form.items[:index], form.items[index+1:]...) } } form.Grid.RemoveComponent(comp) return form } func (form *Form) OnKeyEvent(event KeyEvent) bool { switch event.Key() { case tcell.KeyTab: form.FocusNextItem() return true case tcell.KeyBacktab: form.FocusPreviousItem() return true case tcell.KeyEnter: if form.focused != nil { if fi, ok := form.focused.target.(FormItem); ok { if fi.Submit(event) { form.FocusNextItem() return true } else { return false } } } } return form.Grid.OnKeyEvent(event) } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/go.mod000066400000000000000000000006301460551007700214500ustar00rootroot00000000000000module go.mau.fi/mauview go 1.18 require ( github.com/mattn/go-runewidth v0.0.14 github.com/rivo/uniseg v0.4.2 github.com/zyedidia/clipboard v1.0.4 go.mau.fi/tcell v0.4.0 ) require ( github.com/gdamore/encoding v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect golang.org/x/sys v0.2.0 // indirect golang.org/x/term v0.2.0 // indirect golang.org/x/text v0.4.0 // indirect ) golang-go.mau-mauview-0.2.1+git20240409+f020cbb/go.sum000066400000000000000000000031621460551007700215000ustar00rootroot00000000000000github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.2 h1:YwD0ulJSJytLpiaWua0sBDusfsCZohxjxzVTYjwxfV8= github.com/rivo/uniseg v0.4.2/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/zyedidia/clipboard v1.0.4 h1:r6GUQOyPtIaApRLeD56/U+2uJbXis6ANGbKWCljULEo= github.com/zyedidia/clipboard v1.0.4/go.mod h1:zykFnZUXX0ErxqvYLUFEq7QDJKId8rmh2FgD0/Y8cjA= go.mau.fi/tcell v0.4.0 h1:IPFKhkzF3yZkcRYjzgYBWWiW0JWPTwEBoXlWTBT8o/4= go.mau.fi/tcell v0.4.0/go.mod h1:77zV/6KL4Zip1u9ndjswACmu/LWwZ/oe3BE188uWMrA= golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang-go.mau-mauview-0.2.1+git20240409+f020cbb/grid.go000066400000000000000000000147431460551007700216300ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2019 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package mauview import ( "go.mau.fi/tcell" ) type gridChild struct { genericChild relWidth int relHeight int relX int relY int } type Grid struct { screen Screen children []*gridChild focused *gridChild focusReceived bool prevWidth int prevHeight int forceResize bool columnWidths []int rowHeights []int onFocusChanged func(from, to Component) } func NewGrid() *Grid { return &Grid{ children: []*gridChild{}, focused: nil, prevWidth: -1, prevHeight: -1, forceResize: false, columnWidths: []int{-1}, rowHeights: []int{-1}, } } func extend(arr []int, newSize int) []int { newArr := make([]int, newSize) copy(newArr, arr) for i := len(arr); i < len(newArr); i++ { newArr[i] = -1 } return newArr } func (form *Form) SetOnFocusChanged(fn func(from, to Component)) *Form { form.onFocusChanged = fn return form } func (grid *Grid) createChild(comp Component, x, y, width, height int) *gridChild { if x+width >= len(grid.columnWidths) { grid.columnWidths = extend(grid.columnWidths, x+width) } if y+height >= len(grid.rowHeights) { grid.rowHeights = extend(grid.rowHeights, y+height) } return &gridChild{ genericChild: genericChild{ screen: &ProxyScreen{Parent: grid.screen, Style: tcell.StyleDefault}, target: comp, }, relWidth: width, relHeight: height, relX: x, relY: y, } } func (grid *Grid) addChild(child *gridChild) { if child.relX+child.relWidth >= len(grid.columnWidths) { grid.columnWidths = extend(grid.columnWidths, child.relX+child.relWidth) } if child.relY+child.relHeight >= len(grid.rowHeights) { grid.rowHeights = extend(grid.rowHeights, child.relY+child.relHeight) } grid.children = append(grid.children, child) grid.forceResize = true } func (grid *Grid) AddComponent(comp Component, x, y, width, height int) *Grid { grid.addChild(grid.createChild(comp, x, y, width, height)) return grid } func (grid *Grid) RemoveComponent(comp Component) *Grid { for index := len(grid.children) - 1; index >= 0; index-- { if grid.children[index].target == comp { grid.children = append(grid.children[:index], grid.children[index+1:]...) } } return grid } func (grid *Grid) SetColumn(col, width int) *Grid { if col >= len(grid.columnWidths) { grid.columnWidths = extend(grid.columnWidths, col+1) } grid.columnWidths[col] = width return grid } func (grid *Grid) SetRow(row, height int) *Grid { if row >= len(grid.rowHeights) { grid.rowHeights = extend(grid.rowHeights, row+1) } grid.rowHeights[row] = height return grid } func (grid *Grid) SetColumns(columns []int) *Grid { grid.columnWidths = columns return grid } func (grid *Grid) SetRows(rows []int) *Grid { grid.rowHeights = rows return grid } func pnSum(arr []int) (int, int) { positive := 0 negative := 0 for _, i := range arr { if i < 0 { negative -= i } else { positive += i } } return positive, negative } func fillDynamic(arr []int, size, dynamicItems int) []int { if dynamicItems == 0 { return arr } part := size / dynamicItems remainder := size % dynamicItems newArr := make([]int, len(arr)) for i, val := range arr { if val < 0 { newArr[i] = part * -val if remainder > 0 { remainder-- newArr[i]++ } } else { newArr[i] = val } } return newArr } func (grid *Grid) OnResize(width, height int) { absColWidth, dynamicColumns := pnSum(grid.columnWidths) columnWidths := fillDynamic(grid.columnWidths, width-absColWidth, dynamicColumns) absRowHeight, dynamicRows := pnSum(grid.rowHeights) rowHeights := fillDynamic(grid.rowHeights, height-absRowHeight, dynamicRows) for _, child := range grid.children { child.screen.OffsetX, _ = pnSum(columnWidths[:child.relX]) child.screen.OffsetY, _ = pnSum(rowHeights[:child.relY]) child.screen.Width, _ = pnSum(columnWidths[child.relX : child.relX+child.relWidth]) child.screen.Height, _ = pnSum(rowHeights[child.relY : child.relY+child.relHeight]) } grid.prevWidth, grid.prevHeight = width, height } func (grid *Grid) Draw(screen Screen) { width, height := screen.Size() if grid.forceResize || grid.prevWidth != width || grid.prevHeight != height { grid.OnResize(screen.Size()) } grid.forceResize = false screen.Clear() screenChanged := false if screen != grid.screen { grid.screen = screen screenChanged = true } for _, child := range grid.children { if screenChanged { child.screen.Parent = screen } if grid.focused == nil || child != grid.focused { child.target.Draw(child.screen) } } if grid.focused != nil { grid.focused.target.Draw(grid.focused.screen) } } func (grid *Grid) OnKeyEvent(event KeyEvent) bool { if grid.focused != nil { return grid.focused.target.OnKeyEvent(event) } return false } func (grid *Grid) OnPasteEvent(event PasteEvent) bool { if grid.focused != nil { return grid.focused.target.OnPasteEvent(event) } return false } func (grid *Grid) setFocused(item *gridChild) { if grid.focused != nil { grid.focused.Blur() } var prevFocus, newFocus Component if grid.focused != nil { prevFocus = grid.focused.target } if item != nil { newFocus = item.target } grid.focused = item if grid.focusReceived && grid.focused != nil { grid.focused.Focus() } if grid.onFocusChanged != nil { grid.onFocusChanged(prevFocus, newFocus) } } func (grid *Grid) OnMouseEvent(event MouseEvent) bool { if grid.focused != nil && grid.focused.screen.IsInArea(event.Position()) { screen := grid.focused.screen return grid.focused.target.OnMouseEvent(OffsetMouseEvent(event, -screen.OffsetX, -screen.OffsetY)) } for _, child := range grid.children { if child.screen.IsInArea(event.Position()) { focusChanged := false if event.Buttons() == tcell.Button1 && !event.HasMotion() { grid.setFocused(child) focusChanged = true } return child.target.OnMouseEvent(OffsetMouseEvent(event, -child.screen.OffsetX, -child.screen.OffsetY)) || focusChanged } } if event.Buttons() == tcell.Button1 && !event.HasMotion() && grid.focused != nil { grid.setFocused(nil) return true } return false } func (grid *Grid) Focus() { grid.focusReceived = true if grid.focused != nil { grid.focused.Focus() } } func (grid *Grid) Blur() { if grid.focused != nil { grid.setFocused(nil) } grid.focusReceived = false } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/group.go000066400000000000000000000011251460551007700220250ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2019 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package mauview type genericChild struct { screen *ProxyScreen target Component } func (child genericChild) Focus() { focusable, ok := child.target.(Focusable) if ok { focusable.Focus() } } func (child genericChild) Blur() { focusable, ok := child.target.(Focusable) if ok { focusable.Blur() } } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/inputarea.go000066400000000000000000001023551460551007700226700ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2019 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package mauview import ( "strings" "time" "github.com/mattn/go-runewidth" "github.com/zyedidia/clipboard" "go.mau.fi/tcell" ) // InputArea is a multi-line user-editable text area. type InputArea struct { // Cursor position as the runewidth from the start of the input area text. cursorOffsetW int // Cursor offset from the left of the input area. cursorOffsetX int // Cursor offset from the top of the text. cursorOffsetY int // Number of lines (from top) to offset rendering. viewOffsetY int // The start of the selection as the runewidth from the start of the input area text. selectionStartW int // The end of the selection. selectionEndW int // The text that was entered. text string // The text split into lines. Updated each during each render. lines []string // The text to be displayed in the input area when it is empty. placeholder string // The background color of the input area. fieldBackgroundColor tcell.Color // The text color of the input area. fieldTextColor tcell.Color // The text color of the placeholder. placeholderTextColor tcell.Color // The text color of selected text. selectionTextColor tcell.Color // The background color of selected text. selectionBackgroundColor tcell.Color // Whether or not to enable vim-style keybindings. vimBindings bool // Whether or not text should be automatically copied to the primary clipboard when selected. // Most apps on Linux work this way. copySelection bool // Whether or not the input area is focused. focused bool drawPrepared bool // An optional function which is called when the input has changed. changed func(text string) // An optional function which is called when the user presses tab. tabComplete func(text string, pos int) // An optional function which is called when the user presses the down arrow at the end of the input area. pressKeyDownAtEnd func() // An optional function which is called when the user presses the up arrow at the beginning of the input area. pressKeyUpAtStart func() // Change history for undo/redo functionality. history []*inputAreaSnapshot // Current position in the history array for redo functionality. historyPtr int // Maximum number of history snapshots to keep. historyMaxSize int // Maximum delay (ms) between changes to edit the previous snapshot instead of creating a new one. historyMaxEditDelay int64 // Maximum age (ms) of the previous snapshot to edit the previous snapshot instead of craeting a new one. historyMaxSnapshotAge int64 // Timestamp of the last click used for detecting double clicks. lastClick int64 // Position of the last click used for detecting double clicks. lastClickX int lastClickY int // Number of clicks done within doubleClickTimeout of eachother. clickStreak int // Maximum delay (ms) between clicks to count as a double click. doubleClickTimeout int64 // The previous word start and end X position that the mouse was dragged over when selecting words at a time. // Used to detect if the mouse is still over the same word. lastWordSelectionExtendXStart int lastWordSelectionExtendXEnd int // The position where the current selection streak started. // Used to properly handle the user selecting text backwards. selectionStreakStartWStart int selectionStreakStartWEnd int selectionStreakStartXStart int selectionStreakStartY int } // NewInputArea returns a new input field. func NewInputArea() *InputArea { return &InputArea{ fieldBackgroundColor: Styles.PrimitiveBackgroundColor, fieldTextColor: Styles.PrimaryTextColor, placeholderTextColor: Styles.SecondaryTextColor, selectionTextColor: Styles.PrimaryTextColor, selectionBackgroundColor: Styles.ContrastBackgroundColor, vimBindings: false, copySelection: true, focused: false, selectionEndW: -1, selectionStartW: -1, history: []*inputAreaSnapshot{{"", 0, 0, 0, true}}, historyPtr: 0, historyMaxSize: 256, historyMaxEditDelay: 1 * 1000, historyMaxSnapshotAge: 3 * 1000, lastClick: 0, doubleClickTimeout: 1 * 500, } } // SetText sets the current text of the input field. func (field *InputArea) SetText(text string) *InputArea { field.text = text if field.changed != nil { field.changed(text) } field.snapshot(true) return field } // SetTextAndMoveCursor sets the current text of the input field and moves the cursor with the width difference. func (field *InputArea) SetTextAndMoveCursor(text string) *InputArea { oldWidth := iaStringWidth(field.text) field.text = text newWidth := iaStringWidth(field.text) if oldWidth != newWidth { field.cursorOffsetW += newWidth - oldWidth } if field.changed != nil { field.changed(field.text) } field.snapshot(true) return field } // GetText returns the current text of the input field. func (field *InputArea) GetText() string { return field.text } // SetPlaceholder sets the text to be displayed when the input text is empty. func (field *InputArea) SetPlaceholder(text string) *InputArea { field.placeholder = text return field } // SetBackgroundColor sets the background color of the input area. func (field *InputArea) SetBackgroundColor(color tcell.Color) *InputArea { field.fieldBackgroundColor = color return field } // SetTextColor sets the text color of the input area. func (field *InputArea) SetTextColor(color tcell.Color) *InputArea { field.fieldTextColor = color return field } // SetPlaceholderTextColor sets the text color of placeholder text. func (field *InputArea) SetPlaceholderTextColor(color tcell.Color) *InputArea { field.placeholderTextColor = color return field } // SetChangedFunc sets a handler which is called whenever the text of the input // field has changed. It receives the current text (after the change). func (field *InputArea) SetChangedFunc(handler func(text string)) *InputArea { field.changed = handler return field } func (field *InputArea) SetTabCompleteFunc(handler func(text string, cursorOffset int)) *InputArea { field.tabComplete = handler return field } func (field *InputArea) SetPressKeyUpAtStartFunc(handler func()) *InputArea { field.pressKeyUpAtStart = handler return field } func (field *InputArea) SetPressKeyDownAtEndFunc(handler func()) *InputArea { field.pressKeyDownAtEnd = handler return field } // GetTextHeight returns the number of lines in the text during the previous render. func (field *InputArea) GetTextHeight() int { return len(field.lines) } // inputAreaSnapshot is a single history snapshot of the input area state. type inputAreaSnapshot struct { text string cursorOffsetW int origTimestamp int64 editTimestamp int64 locked bool } func millis() int64 { return time.Now().UnixNano() / 1e6 } // Snapshot saves the current editor state into undo history. func (field *InputArea) snapshot(forceNew bool) { cur := field.history[field.historyPtr] now := millis() if cur.locked || forceNew || now > cur.editTimestamp+field.historyMaxEditDelay || now > cur.origTimestamp+field.historyMaxSnapshotAge { newSnapshot := &inputAreaSnapshot{ text: field.text, cursorOffsetW: field.cursorOffsetW, origTimestamp: now, editTimestamp: now, } if len(field.history) >= field.historyMaxSize { field.history = append(field.history[1:field.historyPtr+1], newSnapshot) } else { field.history = append(field.history[0:field.historyPtr+1], newSnapshot) field.historyPtr++ } } else { cur.text = field.text cur.cursorOffsetW = field.cursorOffsetW cur.editTimestamp = now } } // Redo reverses an undo. func (field *InputArea) Redo() { if field.historyPtr >= len(field.history)-1 { return } field.historyPtr++ newCur := field.history[field.historyPtr] newCur.locked = true field.text = newCur.text field.cursorOffsetW = newCur.cursorOffsetW } // Undo reverses the input area to the previous history snapshot. func (field *InputArea) Undo() { if field.historyPtr == 0 { return } field.historyPtr-- newCur := field.history[field.historyPtr] newCur.locked = true field.text = newCur.text field.cursorOffsetW = newCur.cursorOffsetW } // recalculateCursorOffset recalculates the runewidth cursor offset based on the X and Y cursor offsets. func (field *InputArea) recalculateCursorOffset() { cursorOffsetW := 0 for i, str := range field.lines { ln := iaStringWidth(str) if i < field.cursorOffsetY { cursorOffsetW += ln } else { if ln == 0 { break } else if str[len(str)-1] == '\n' { ln-- } if field.cursorOffsetX < ln { cursorOffsetW += field.cursorOffsetX } else { cursorOffsetW += ln } break } } field.cursorOffsetW = cursorOffsetW textWidth := iaStringWidth(field.text) if field.cursorOffsetW > textWidth { field.cursorOffsetW = textWidth field.recalculateCursorPos() } } // recalculateCursorPos recalculates the X and Y cursor offsets based on the runewidth cursor offset. func (field *InputArea) recalculateCursorPos() { cursorOffsetY := 0 cursorOffsetX := field.cursorOffsetW for i, str := range field.lines { if cursorOffsetX >= iaStringWidth(str) { cursorOffsetX -= iaStringWidth(str) } else { cursorOffsetY = i break } } field.cursorOffsetX = cursorOffsetX field.cursorOffsetY = cursorOffsetY } func matchBoundaryPattern(extract string) string { matches := boundaryPattern.FindAllStringIndex(extract, -1) if len(matches) > 0 { if match := matches[len(matches)-1]; len(match) >= 2 { if until := match[1]; until < len(extract) { extract = extract[:until] } } } return extract } // prepareText splits the text into lines that fit the input area. func (field *InputArea) prepareText(width int) { var lines []string if len(field.text) == 0 { field.lines = lines return } forcedLinebreaks := strings.Split(field.text, "\n") for _, str := range forcedLinebreaks { str = str + "\n" // Adapted from tview/textview.go#reindexBuffer() for len(str) > 0 { extract := iaSubstringBefore(str, width-1) if len(extract) < len(str) { if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 { extract = str[:len(extract)+spaces[1]] } extract = matchBoundaryPattern(extract) } lines = append(lines, extract) str = str[len(extract):] } } field.lines = lines } // updateViewOffset updates the view offset so that: // - it is not negative // - it is not unnecessarily high // - the cursor is within the rendered area func (field *InputArea) updateViewOffset(height int) { if field.viewOffsetY < 0 { field.viewOffsetY = 0 } else if len(field.lines) > height && field.viewOffsetY+height > len(field.lines) { field.viewOffsetY = len(field.lines) - height } if field.cursorOffsetY-field.viewOffsetY < 0 { field.viewOffsetY = field.cursorOffsetY } else if field.cursorOffsetY >= field.viewOffsetY+height { field.viewOffsetY = field.cursorOffsetY - height + 1 } } // drawText draws the text and the cursor. func (field *InputArea) drawText(screen Screen) { width, height := screen.Size() if len(field.lines) == 0 { if len(field.placeholder) > 0 { Print(screen, field.placeholder, 0, 0, width, AlignLeft, field.placeholderTextColor) } return } defaultStyle := tcell.StyleDefault.Foreground(field.fieldTextColor).Background(field.fieldBackgroundColor) highlightStyle := defaultStyle.Foreground(field.selectionTextColor).Background(field.selectionBackgroundColor) rwOffset := 0 for y := 0; y <= field.viewOffsetY+height && y < len(field.lines); y++ { if y < field.viewOffsetY { rwOffset += iaStringWidth(field.lines[y]) continue } x := 0 for _, ch := range []rune(field.lines[y]) { w := iaRuneWidth(ch) var style tcell.Style if rwOffset >= field.selectionStartW && rwOffset < field.selectionEndW { style = highlightStyle } else { style = defaultStyle } rwOffset += w for w > 0 { screen.SetContent(x, y-field.viewOffsetY, ch, nil, style) x++ w-- } } } } func (field *InputArea) PrepareDraw(width int) { field.prepareText(width) field.recalculateCursorPos() } // Draw draws this primitive onto the screen. func (field *InputArea) Draw(screen Screen) { width, height := screen.Size() if height < 1 || width < 1 { return } if !field.drawPrepared { field.PrepareDraw(width) } field.updateViewOffset(height) screen.SetStyle(tcell.StyleDefault.Background(field.fieldBackgroundColor)) screen.Clear() field.drawText(screen) if field.focused && field.selectionEndW == -1 { screen.ShowCursor(field.cursorOffsetX, field.cursorOffsetY-field.viewOffsetY) } field.drawPrepared = false } func iaRuneWidth(ch rune) int { if ch == '\n' { return 1 } return runewidth.RuneWidth(ch) } func iaStringWidth(s string) (width int) { w := runewidth.StringWidth(s) for _, ch := range s { if ch == '\n' { w++ } } return w } func iaSubstringBefore(s string, w int) string { if iaStringWidth(s) <= w { return s } r := []rune(s) //tw := iaStringWidth(tail) //w -= tw width := 0 i := 0 for ; i < len(r); i++ { cw := iaRuneWidth(r[i]) if width+cw > w { break } width += cw } return string(r[0:i]) // + tail } // TypeRune inserts the given rune at the current cursor position. func (field *InputArea) TypeRune(ch rune) { var left, right string if field.selectionEndW != -1 { left = iaSubstringBefore(field.text, field.selectionStartW) rightLeft := iaSubstringBefore(field.text, field.selectionEndW) right = field.text[len(rightLeft):] field.cursorOffsetW = field.selectionStartW } else { left = iaSubstringBefore(field.text, field.cursorOffsetW) right = field.text[len(left):] } field.text = left + string(ch) + right field.cursorOffsetW += iaRuneWidth(ch) field.selectionEndW = -1 field.selectionStartW = -1 } // MoveCursorLeft moves the cursor left. // // If moveWord is true, the cursor moves a whole word to the left. // // If extendSelection is true, the selection is either extended to the left if the cursor is on the left side of the // selection or retracted from the right if the cursor is on the right side. If there is no existing selection, the // selection will be created towards the left of the cursor. func (field *InputArea) MoveCursorLeft(moveWord, extendSelection bool) { before := iaSubstringBefore(field.text, field.cursorOffsetW) var diff int if moveWord { diff = -iaStringWidth(lastWord.FindString(before)) } else if len(before) > 0 { beforeRunes := []rune(before) char := beforeRunes[len(beforeRunes)-1] diff = -iaRuneWidth(char) } if extendSelection { field.extendSelection(diff) } else { field.moveCursor(diff) } } // MoveCursorRight moves the cursor right. // // If moveWord is true, the cursor moves a whole word to the right. // // If extendSelection is true, the selection is either extended to the right if the cursor is on the right side of the // selection or retracted from the left if the cursor is on the left side. If there is no existing selection, the // selection will be created towards the right of the cursor. func (field *InputArea) MoveCursorRight(moveWord, extendSelection bool) { before := iaSubstringBefore(field.text, field.cursorOffsetW) after := field.text[len(before):] var diff int if moveWord { diff = +iaStringWidth(firstWord.FindString(after)) } else if len(after) > 0 { char := []rune(after)[0] diff = +iaRuneWidth(char) } if extendSelection { field.extendSelection(diff) } else { field.moveCursor(diff) } } func (field *InputArea) MoveCursorHome(extendSelection bool) { if extendSelection { field.extendSelection(-iaStringWidth(iaSubstringBefore(field.text, field.cursorOffsetW))) } else { field.selectionEndW = -1 field.selectionStartW = -1 field.cursorOffsetW = 0 } } func (field *InputArea) MoveCursorEnd(extendSelection bool) { if extendSelection { after := field.text[len(iaSubstringBefore(field.text, field.cursorOffsetW)):] field.extendSelection(iaStringWidth(after)) } else { field.selectionEndW = -1 field.selectionStartW = -1 field.cursorOffsetW = iaStringWidth(field.text) } } // moveCursor resets the selection and adjusts the runewidth cursor offset. func (field *InputArea) moveCursor(diff int) { field.selectionEndW = -1 field.selectionStartW = -1 field.cursorOffsetW += diff } // extendSelection adjusts the selection or creates a selection. Negative values make the selection go left and // positive values make the selection go right. // "Go" in context of a selection means retracting or extending depending on which side the cursor is on. func (field *InputArea) extendSelection(diff int) { if field.selectionEndW == -1 { field.selectionStartW = field.cursorOffsetW field.selectionEndW = field.selectionStartW + diff } else if field.cursorOffsetW == field.selectionEndW { field.selectionEndW += diff } else if field.cursorOffsetW == field.selectionStartW { field.selectionStartW += diff } field.cursorOffsetW += diff if field.selectionStartW > field.selectionEndW { field.selectionStartW, field.selectionEndW = field.selectionEndW, field.selectionStartW } field.copy("primary", false) } // MoveCursorUp moves the cursor up one line. // // If extendSelection is true, the selection is either extended up if the cursor is at the beginning of the selection or // retracted from the bottom if the cursor is at the end of the selection. func (field *InputArea) MoveCursorUp(extendSelection bool) { pX, pY := field.cursorOffsetX, field.cursorOffsetY if field.cursorOffsetY > 0 { field.cursorOffsetY-- lineWidth := iaStringWidth(field.lines[field.cursorOffsetY]) if lineWidth < field.cursorOffsetX { field.cursorOffsetX = lineWidth } } else { field.cursorOffsetX = 0 } if extendSelection && len(field.lines) > 0 { prevLineBefore := iaSubstringBefore(field.lines[pY], pX) curLineBefore := iaSubstringBefore(field.lines[field.cursorOffsetY], field.cursorOffsetX) curLineAfter := field.lines[field.cursorOffsetY][len(curLineBefore):] field.extendSelection(-iaStringWidth(curLineAfter + prevLineBefore)) } else { field.selectionStartW = -1 field.selectionEndW = -1 } prevOffsetW := field.cursorOffsetW field.recalculateCursorOffset() if field.cursorOffsetW == prevOffsetW && field.pressKeyUpAtStart != nil { field.pressKeyUpAtStart() } } // MoveCursorDown moves the cursor down one line. // // If extendSelection is true, the selection is either extended down if the cursor is at the end of the selection or // retracted from the top if the cursor is at the beginning of the selection. func (field *InputArea) MoveCursorDown(extendSelection bool) { pX, pY := field.cursorOffsetX, field.cursorOffsetY if field.cursorOffsetY < len(field.lines)-1 { field.cursorOffsetY++ lineWidth := iaStringWidth(field.lines[field.cursorOffsetY]) if lineWidth < field.cursorOffsetX { field.cursorOffsetX = lineWidth } } else if field.cursorOffsetY == len(field.lines)-1 { lineWidth := iaStringWidth(field.lines[field.cursorOffsetY]) field.cursorOffsetX = lineWidth } if extendSelection && len(field.lines) > 0 { prevLineBefore := iaSubstringBefore(field.lines[pY], pX) prevLineAfter := field.lines[pY][len(prevLineBefore):] curLineBefore := iaSubstringBefore(field.lines[field.cursorOffsetY], field.cursorOffsetX) field.extendSelection(iaStringWidth(prevLineAfter + curLineBefore)) } else { field.selectionStartW = -1 field.selectionEndW = -1 } prevOffsetW := field.cursorOffsetW field.recalculateCursorOffset() if field.cursorOffsetW == prevOffsetW && field.pressKeyDownAtEnd != nil { field.pressKeyDownAtEnd() } } // SetCursorPos sets the X and Y cursor offsets. func (field *InputArea) SetCursorPos(x, y int) { field.cursorOffsetX = x field.cursorOffsetY = y field.selectionStartW = -1 field.selectionEndW = -1 if field.cursorOffsetY > len(field.lines) { field.cursorOffsetY = len(field.lines) - 1 } field.recalculateCursorOffset() } func (field *InputArea) GetCursorPos() (int, int) { return field.cursorOffsetX, field.cursorOffsetY } // SetCursorOffset sets the runewidth cursor offset. func (field *InputArea) SetCursorOffset(offset int) { if offset < 0 { offset = iaStringWidth(field.text) - (offset + 1) } field.cursorOffsetW = offset field.selectionStartW = -1 field.selectionEndW = -1 } func (field *InputArea) GetCursorOffset() int { return field.cursorOffsetW } func (field *InputArea) SetSelection(start, end int) { field.selectionStartW = start field.selectionEndW = end } func (field *InputArea) GetSelectedText() string { leftLeft := iaSubstringBefore(field.text, field.selectionStartW) rightLeft := iaSubstringBefore(field.text, field.selectionEndW) return rightLeft[len(leftLeft):] } func (field *InputArea) GetSelection() (int, int) { return field.selectionStartW, field.selectionEndW } func (field *InputArea) ClearSelection() { field.selectionStartW = -1 field.selectionEndW = -1 } // findWordAt finds the word around the given runewidth offset in the given string. // // Returns the start and end index of the word. func findWordAt(line string, x int) (beforePos, afterPos int) { before := iaSubstringBefore(line, x) after := line[len(before):] afterBound := boundaryPattern.FindStringIndex(after) if afterBound != nil { afterPos = afterBound[0] } else { afterPos = len(after) } afterPos += len(before) beforeBounds := boundaryPattern.FindAllStringIndex(before, -1) if len(beforeBounds) > 0 { beforeBound := beforeBounds[len(beforeBounds)-1] beforePos = beforeBound[1] } else { beforePos = 0 } return } // startSelectionStreak selects the current word or line for double and triple clicks (respectively). func (field *InputArea) startSelectionStreak(x, y int) { field.cursorOffsetY = y if field.cursorOffsetY > len(field.lines) { field.cursorOffsetY = len(field.lines) - 1 } else if len(field.lines) == 0 { return } line := field.lines[field.cursorOffsetY] fullLine := (field.clickStreak-2)%2 == 1 if fullLine { field.cursorOffsetX = iaStringWidth(line) field.recalculateCursorOffset() field.selectionStartW = field.cursorOffsetW - field.cursorOffsetX field.selectionEndW = field.cursorOffsetW } else { beforePos, afterPos := findWordAt(line, x) field.cursorOffsetX = iaStringWidth(line[:afterPos]) field.recalculateCursorOffset() wordWidth := iaStringWidth(line[beforePos:afterPos]) field.selectionStartW = field.cursorOffsetW - wordWidth field.selectionEndW = field.cursorOffsetW field.selectionStreakStartWStart = field.selectionStartW field.selectionStreakStartWEnd = field.selectionEndW field.selectionStreakStartXStart = field.cursorOffsetX - wordWidth } field.selectionStreakStartY = field.cursorOffsetY field.copy("primary", false) } // ExtendSelection extends the selection as if the user dragged their mouse to the given coordinates. func (field *InputArea) ExtendSelection(x, y int) { field.cursorOffsetY = y if field.cursorOffsetY > len(field.lines) { field.cursorOffsetY = len(field.lines) - 1 } if field.clickStreak <= 1 { field.cursorOffsetX = x } else if (field.clickStreak-2)%2 == 0 { if field.lastClickY == y && x >= field.lastWordSelectionExtendXStart && x <= field.lastWordSelectionExtendXEnd { return } line := field.lines[field.cursorOffsetY] beforePos, afterPos := findWordAt(line, x) field.lastWordSelectionExtendXStart = beforePos field.lastWordSelectionExtendXEnd = afterPos if y < field.selectionStreakStartY || (y == field.selectionStreakStartY && x < field.selectionStreakStartXStart) { field.cursorOffsetW = field.selectionStartW field.selectionEndW = field.selectionStreakStartWEnd field.cursorOffsetX = iaStringWidth(line[:beforePos]) } else { field.cursorOffsetW = field.selectionEndW field.selectionStartW = field.selectionStreakStartWStart field.cursorOffsetX = iaStringWidth(line[:afterPos]) } } else { if field.lastClickY == y { return } if field.cursorOffsetY == field.selectionStreakStartY { // Special case to not mess up stuff when dragging mouse over selection streak start. line := field.lines[field.cursorOffsetY] field.cursorOffsetX = iaStringWidth(line) field.recalculateCursorOffset() field.selectionStartW = field.cursorOffsetW - field.cursorOffsetX field.selectionEndW = field.cursorOffsetW return } else if field.cursorOffsetY < field.selectionStreakStartY { field.cursorOffsetW = field.selectionStartW field.cursorOffsetX = 0 } else { field.cursorOffsetW = field.selectionEndW line := field.lines[field.cursorOffsetY] field.cursorOffsetX = iaStringWidth(line) } } prevOffset := field.cursorOffsetW field.recalculateCursorOffset() if field.selectionEndW == -1 { field.selectionStartW = prevOffset field.selectionEndW = field.cursorOffsetW } else if prevOffset == field.selectionEndW { field.selectionEndW = field.cursorOffsetW } else { field.selectionStartW = field.cursorOffsetW } if field.selectionStartW > field.selectionEndW { field.selectionStartW, field.selectionEndW = field.selectionEndW, field.selectionStartW } field.copy("primary", false) } // RemoveNextCharacter removes the character after the cursor. func (field *InputArea) RemoveNextCharacter() { if field.selectionEndW > 0 { field.RemoveSelection() return } else if field.cursorOffsetW >= iaStringWidth(field.text) { return } left := iaSubstringBefore(field.text, field.cursorOffsetW) // Take everything after the left part minus the first character. right := string([]rune(field.text[len(left):])[1:]) field.text = left + right } // RemovePreviousWord removes the word before the cursor. func (field *InputArea) RemovePreviousWord() { if field.selectionEndW > 0 { field.RemoveSelection() return } left := iaSubstringBefore(field.text, field.cursorOffsetW) replacement := lastWord.ReplaceAllString(left, "") field.text = replacement + field.text[len(left):] field.cursorOffsetW = iaStringWidth(replacement) } // RemoveSelection removes the selected content. func (field *InputArea) RemoveSelection() { leftLeft := iaSubstringBefore(field.text, field.selectionStartW) rightLeft := iaSubstringBefore(field.text, field.selectionEndW) rightRight := field.text[len(rightLeft):] field.text = leftLeft + rightRight if field.cursorOffsetW == field.selectionEndW { field.cursorOffsetW -= iaStringWidth(rightLeft[len(leftLeft):]) } field.selectionEndW = -1 field.selectionStartW = -1 } // RemovePreviousCharacter removes the character before the cursor. func (field *InputArea) RemovePreviousCharacter() { if field.selectionEndW > 0 { field.RemoveSelection() return } else if field.cursorOffsetW == 0 { return } left := iaSubstringBefore(field.text, field.cursorOffsetW) right := field.text[len(left):] // Take everything before the right part minus the last character. leftRunes := []rune(left) leftRunes = leftRunes[0 : len(leftRunes)-1] left = string(leftRunes) // Figure out what character was removed to correctly decrease cursorOffset. removedChar := field.text[len(left) : len(field.text)-len(right)] field.text = left + right field.cursorOffsetW -= iaStringWidth(removedChar) } // Clear clears the input area. func (field *InputArea) Clear() { field.text = "" field.cursorOffsetW = 0 field.cursorOffsetX = 0 field.cursorOffsetY = 0 field.selectionEndW = -1 field.selectionStartW = -1 field.viewOffsetY = 0 } // SelectAll extends the selection to cover all text in the input area. func (field *InputArea) SelectAll() { field.selectionStartW = 0 field.selectionEndW = iaStringWidth(field.text) field.cursorOffsetW = field.selectionEndW field.copy("primary", false) } // handleInputChanges calls the text change handler and makes sure // offsets are valid after a change in the text of the input area. func (field *InputArea) handleInputChanges(originalText string) { // Trigger changed events. if field.text != originalText && field.changed != nil { field.changed(field.text) } // Make sure cursor offset is valid if field.cursorOffsetW < 0 { field.cursorOffsetW = 0 } textWidth := iaStringWidth(field.text) if field.cursorOffsetW > textWidth { field.cursorOffsetW = textWidth } if field.selectionEndW > textWidth { field.selectionEndW = textWidth } if field.selectionEndW <= field.selectionStartW { field.selectionStartW = -1 field.selectionEndW = -1 } } // OnPasteEvent handles a terminal bracketed paste event. func (field *InputArea) OnPasteEvent(event PasteEvent) bool { var left, right string if field.selectionEndW != -1 { left = iaSubstringBefore(field.text, field.selectionStartW) rightLeft := iaSubstringBefore(field.text, field.selectionEndW) right = field.text[len(rightLeft):] field.cursorOffsetW = field.selectionStartW } else { left = iaSubstringBefore(field.text, field.cursorOffsetW) right = field.text[len(left):] } oldText := field.text field.text = left + event.Text() + right field.cursorOffsetW += iaStringWidth(event.Text()) field.handleInputChanges(oldText) field.selectionEndW = -1 field.selectionStartW = -1 field.snapshot(true) return true } // Paste reads the clipboard and inserts the content at the cursor position. func (field *InputArea) Paste() { text, _ := clipboard.ReadAll("clipboard") field.OnPasteEvent(customPasteEvent{nil, text}) } // Copy copies the currently selected content onto the clipboard. func (field *InputArea) Copy() { field.copy("clipboard", false) } func (field *InputArea) Cut() { field.copy("clipboard", true) } func (field *InputArea) copy(selection string, cut bool) { if !field.copySelection && selection == "primary" { return } else if field.selectionEndW == -1 { return } left := iaSubstringBefore(field.text, field.selectionStartW) rightLeft := iaSubstringBefore(field.text, field.selectionEndW) text := rightLeft[len(left):] _ = clipboard.WriteAll(text, selection) if cut { field.text = left + field.text[len(rightLeft):] field.cursorOffsetW = field.selectionStartW field.selectionStartW = -1 field.selectionEndW = -1 } } // OnKeyEvent handles a terminal key press event. func (field *InputArea) OnKeyEvent(event KeyEvent) bool { hasMod := func(mod tcell.ModMask) bool { return event.Modifiers()&mod != 0 } oldText := field.text doSnapshot := false forceNewSnapshot := false // Process key event. switch event.Key() { case tcell.KeyRune: field.TypeRune(event.Rune()) doSnapshot = true forceNewSnapshot = event.Rune() == ' ' case tcell.KeyEnter: field.TypeRune('\n') doSnapshot = true forceNewSnapshot = true case tcell.KeyLeft: field.MoveCursorLeft(hasMod(tcell.ModCtrl), hasMod(tcell.ModShift)) case tcell.KeyRight: field.MoveCursorRight(hasMod(tcell.ModCtrl), hasMod(tcell.ModShift)) case tcell.KeyUp: field.MoveCursorUp(hasMod(tcell.ModShift)) case tcell.KeyDown: field.MoveCursorDown(hasMod(tcell.ModShift)) case tcell.KeyHome: field.MoveCursorHome(hasMod(tcell.ModShift)) case tcell.KeyEnd: field.MoveCursorEnd(hasMod(tcell.ModShift)) case tcell.KeyDelete: field.RemoveNextCharacter() doSnapshot = true case tcell.KeyBackspace: if Backspace1RemovesWord { field.RemovePreviousWord() } else { field.RemovePreviousCharacter() } doSnapshot = true forceNewSnapshot = true case tcell.KeyBackspace2: forceNewSnapshot = field.selectionEndW > 0 if Backspace2RemovesWord { field.RemovePreviousWord() } else { field.RemovePreviousCharacter() } doSnapshot = true case tcell.KeyTab: if field.tabComplete != nil { field.tabComplete(field.text, field.cursorOffsetW) } default: if field.vimBindings { switch event.Key() { case tcell.KeyCtrlU: field.Clear() doSnapshot = true forceNewSnapshot = true case tcell.KeyCtrlW: field.RemovePreviousWord() doSnapshot = true forceNewSnapshot = true default: return false } } else { switch event.Key() { case tcell.KeyCtrlA: field.SelectAll() case tcell.KeyCtrlZ: field.Undo() case tcell.KeyCtrlY: field.Redo() case tcell.KeyCtrlC: field.Copy() case tcell.KeyCtrlV: field.Paste() return true case tcell.KeyCtrlX: field.Cut() default: return false } } } field.handleInputChanges(oldText) if doSnapshot { field.snapshot(forceNewSnapshot) } return true } // Focus marks the input area as focused. func (field *InputArea) Focus() { field.focused = true } // Blur marks the input area as not focused. func (field *InputArea) Blur() { field.focused = false } // OnMouseEvent handles a terminal mouse event. func (field *InputArea) OnMouseEvent(event MouseEvent) bool { switch event.Buttons() { case tcell.Button1: cursorX, cursorY := event.Position() cursorY += field.viewOffsetY now := millis() sameCell := field.lastClickX == cursorX && field.lastClickY == cursorY if !event.HasMotion() { withinTimeout := now < field.lastClick+field.doubleClickTimeout if field.clickStreak > 0 && sameCell && withinTimeout { field.clickStreak++ } else { field.clickStreak = 1 } if field.clickStreak <= 1 { field.SetCursorPos(cursorX, cursorY) } else { field.startSelectionStreak(cursorX, cursorY) } field.lastClick = now field.lastClickX = cursorX field.lastClickY = cursorY } else { if sameCell { return false } field.ExtendSelection(cursorX, cursorY) } case tcell.WheelDown: field.viewOffsetY += 3 field.cursorOffsetY += 3 field.recalculateCursorOffset() case tcell.WheelUp: field.viewOffsetY -= 3 field.cursorOffsetY -= 3 field.recalculateCursorOffset() default: return false } return true } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/inputfield.go000066400000000000000000000262441460551007700230450ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2019 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. // Based on https://github.com/rivo/tview/blob/master/inputfield.go package mauview import ( "regexp" "strings" "unicode/utf8" "github.com/mattn/go-runewidth" "go.mau.fi/tcell" ) // InputField is a single-line user-editable text field. // // Use SetMaskCharacter() to hide input from onlookers (e.g. for password // input). type InputField struct { // Cursor position cursorOffset int // Number of characters (from left) to offset rendering. viewOffset int // The text that was entered. text string // The text to be displayed in the input area when it is empty. placeholder string // The background color of the input area. fieldBackgroundColor tcell.Color // The text color of the input area. fieldTextColor tcell.Color // The text color of the placeholder. placeholderTextColor tcell.Color // A character to mask entered text (useful for password fields). A value of 0 // disables masking. maskCharacter rune // Whether or not to enable vim-style keybindings. vimBindings bool // Whether or not the input field is focused. focused bool // An optional function which is called when the input has changed. changed func(text string) // An optional function which is called when the user presses tab. tabComplete func(text string, pos int) } // NewInputField returns a new input field. func NewInputField() *InputField { return &InputField{ fieldBackgroundColor: Styles.ContrastBackgroundColor, fieldTextColor: Styles.PrimaryTextColor, placeholderTextColor: Styles.ContrastSecondaryTextColor, } } // SetText sets the current text of the input field. func (field *InputField) SetText(text string) *InputField { field.text = text if field.changed != nil { field.changed(text) } return field } // SetTextAndMoveCursor sets the current text of the input field and moves the cursor with the width difference. func (field *InputField) SetTextAndMoveCursor(text string) *InputField { oldWidth := runewidth.StringWidth(field.text) field.text = text newWidth := runewidth.StringWidth(field.text) if oldWidth != newWidth { field.cursorOffset += newWidth - oldWidth } if field.changed != nil { field.changed(field.text) } return field } // GetText returns the current text of the input field. func (field *InputField) GetText() string { return field.text } // SetPlaceholder sets the text to be displayed when the input text is empty. func (field *InputField) SetPlaceholder(text string) *InputField { field.placeholder = text return field } // SetBackgroundColor sets the background color of the input area. func (field *InputField) SetBackgroundColor(color tcell.Color) *InputField { field.fieldBackgroundColor = color return field } // SetTextColor sets the text color of the input area. func (field *InputField) SetTextColor(color tcell.Color) *InputField { field.fieldTextColor = color return field } // SetPlaceholderTextColor sets the text color of placeholder text. func (field *InputField) SetPlaceholderTextColor(color tcell.Color) *InputField { field.placeholderTextColor = color return field } // SetMaskCharacter sets a character that masks user input on a screen. A value // of 0 disables masking. func (field *InputField) SetMaskCharacter(mask rune) *InputField { field.maskCharacter = mask return field } // SetChangedFunc sets a handler which is called whenever the text of the input // field has changed. It receives the current text (after the change). func (field *InputField) SetChangedFunc(handler func(text string)) *InputField { field.changed = handler return field } func (field *InputField) SetTabCompleteFunc(handler func(text string, cursorOffset int)) *InputField { field.tabComplete = handler return field } // prepareText prepares the text to be displayed and recalculates the view and cursor offsets. func (field *InputField) prepareText(screen Screen) (text string, placeholder bool) { width, _ := screen.Size() text = field.text if len(text) == 0 && len(field.placeholder) > 0 { text = field.placeholder placeholder = true } if !placeholder && field.maskCharacter > 0 { text = strings.Repeat(string(field.maskCharacter), utf8.RuneCountInString(text)) } textWidth := runewidth.StringWidth(text) if field.cursorOffset >= textWidth { width-- } if field.cursorOffset < field.viewOffset { field.viewOffset = field.cursorOffset } else if field.cursorOffset > field.viewOffset+width { field.viewOffset = field.cursorOffset - width } else if textWidth-field.viewOffset < width { field.viewOffset = textWidth - width } if field.viewOffset < 0 { field.viewOffset = 0 } return } // drawText draws the text and the cursor. func (field *InputField) drawText(screen Screen, text string, placeholder bool) { width, _ := screen.Size() runes := []rune(text) x := 0 style := tcell.StyleDefault.Foreground(field.fieldTextColor).Background(field.fieldBackgroundColor) if placeholder { style = style.Foreground(field.placeholderTextColor) } for pos := field.viewOffset; pos <= width+field.viewOffset && pos < len(runes); pos++ { ch := runes[pos] w := runewidth.RuneWidth(ch) for w > 0 { screen.SetContent(x, 0, ch, nil, style) x++ w-- } } for ; x <= width; x++ { screen.SetContent(x, 0, ' ', nil, style) } } // Draw draws this primitive onto the screen. func (field *InputField) Draw(screen Screen) { width, height := screen.Size() if height < 1 || width < 1 { return } text, placeholder := field.prepareText(screen) field.drawText(screen, text, placeholder) if field.focused { field.setCursor(screen) } } func (field *InputField) GetCursorOffset() int { return field.cursorOffset } func (field *InputField) SetCursorOffset(offset int) *InputField { if offset < 0 { offset = 0 } else { width := runewidth.StringWidth(field.text) if offset >= width { offset = width } } field.cursorOffset = offset return field } // setCursor sets the cursor position. func (field *InputField) setCursor(screen Screen) { width, _ := screen.Size() x := field.cursorOffset - field.viewOffset if x >= width { x = width - 1 } else if x < 0 { x = 0 } screen.ShowCursor(x, 0) } var ( lastWord = regexp.MustCompile(`\S+\s*$`) firstWord = regexp.MustCompile(`^\s*\S+`) ) func SubstringBefore(s string, w int) string { return runewidth.Truncate(s, w, "") } func (field *InputField) TypeRune(ch rune) { leftPart := SubstringBefore(field.text, field.cursorOffset) field.text = leftPart + string(ch) + field.text[len(leftPart):] field.cursorOffset += runewidth.RuneWidth(ch) } func (field *InputField) MoveCursorLeft(moveWord bool) { before := SubstringBefore(field.text, field.cursorOffset) if moveWord { found := lastWord.FindString(before) field.cursorOffset -= runewidth.StringWidth(found) } else if len(before) > 0 { beforeRunes := []rune(before) char := beforeRunes[len(beforeRunes)-1] field.cursorOffset -= runewidth.RuneWidth(char) } } func (field *InputField) MoveCursorRight(moveWord bool) { before := SubstringBefore(field.text, field.cursorOffset) after := field.text[len(before):] if moveWord { found := firstWord.FindString(after) field.cursorOffset += runewidth.StringWidth(found) } else if len(after) > 0 { char := []rune(after)[0] field.cursorOffset += runewidth.RuneWidth(char) } } func (field *InputField) RemoveNextCharacter() { if field.cursorOffset >= runewidth.StringWidth(field.text) { return } leftPart := SubstringBefore(field.text, field.cursorOffset) // Take everything after the left part minus the first character. rightPart := string([]rune(field.text[len(leftPart):])[1:]) field.text = leftPart + rightPart } func (field *InputField) Clear() { field.text = "" field.cursorOffset = 0 field.viewOffset = 0 } func (field *InputField) RemovePreviousWord() { leftPart := SubstringBefore(field.text, field.cursorOffset) rightPart := field.text[len(leftPart):] replacement := lastWord.ReplaceAllString(leftPart, "") field.text = replacement + rightPart field.cursorOffset -= runewidth.StringWidth(leftPart) - runewidth.StringWidth(replacement) } func (field *InputField) RemovePreviousCharacter() { if field.cursorOffset == 0 { return } leftPart := SubstringBefore(field.text, field.cursorOffset) rightPart := field.text[len(leftPart):] // Take everything before the right part minus the last character. leftPartRunes := []rune(leftPart) leftPartRunes = leftPartRunes[0 : len(leftPartRunes)-1] leftPart = string(leftPartRunes) // Figure out what character was removed to correctly decrease cursorOffset. removedChar := field.text[len(leftPart) : len(field.text)-len(rightPart)] field.text = leftPart + rightPart field.cursorOffset -= runewidth.StringWidth(removedChar) } func (field *InputField) handleInputChanges(originalText string) { // Trigger changed events. if field.text != originalText && field.changed != nil { field.changed(field.text) } // Make sure cursor offset is valid if field.cursorOffset < 0 { field.cursorOffset = 0 } width := runewidth.StringWidth(field.text) if field.cursorOffset > width { field.cursorOffset = width } } func (field *InputField) OnPasteEvent(event PasteEvent) bool { defer field.handleInputChanges(field.text) leftPart := SubstringBefore(field.text, field.cursorOffset) field.text = leftPart + event.Text() + field.text[len(leftPart):] field.cursorOffset += runewidth.StringWidth(event.Text()) return true } func (field *InputField) Submit(event KeyEvent) bool { return true } // Global options to specify which of the two backspace key codes should remove the whole previous word. // If false, only the previous character will be removed with that key code. var ( Backspace1RemovesWord = true Backspace2RemovesWord = false ) func (field *InputField) OnKeyEvent(event KeyEvent) bool { defer field.handleInputChanges(field.text) // Process key event. switch key := event.Key(); key { case tcell.KeyRune: field.TypeRune(event.Rune()) case tcell.KeyLeft: field.MoveCursorLeft(event.Modifiers() == tcell.ModCtrl) case tcell.KeyRight: field.MoveCursorRight(event.Modifiers() == tcell.ModCtrl) case tcell.KeyDelete: field.RemoveNextCharacter() case tcell.KeyCtrlU: if field.vimBindings { field.Clear() } case tcell.KeyCtrlW: if field.vimBindings { field.RemovePreviousWord() } case tcell.KeyBackspace: if Backspace1RemovesWord { field.RemovePreviousWord() } else { field.RemovePreviousCharacter() } case tcell.KeyBackspace2: if Backspace2RemovesWord { field.RemovePreviousWord() } else { field.RemovePreviousCharacter() } case tcell.KeyTab: if field.tabComplete != nil { field.tabComplete(field.text, field.cursorOffset) return true } return false default: return false } return true } func (field *InputField) Focus() { field.focused = true } func (field *InputField) Blur() { field.focused = false } func (field *InputField) OnMouseEvent(event MouseEvent) bool { if event.Buttons() == tcell.Button1 { x, _ := event.Position() field.SetCursorOffset(field.viewOffset + x) return true } return false } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/mauview-test/000077500000000000000000000000001460551007700227755ustar00rootroot00000000000000golang-go.mau-mauview-0.2.1+git20240409+f020cbb/mauview-test/debug/000077500000000000000000000000001460551007700240635ustar00rootroot00000000000000golang-go.mau-mauview-0.2.1+git20240409+f020cbb/mauview-test/debug/debug.go000066400000000000000000000026171460551007700255060ustar00rootroot00000000000000// gomuks - A terminal Matrix client written in Go. // Copyright (C) 2018 Tulir Asokan // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package debug import ( "fmt" "io" "os" "runtime/debug" "time" ) var writer io.Writer func init() { var err error writer, err = os.OpenFile("/tmp/mauview-debug.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) if err != nil { panic(err) } } func Printf(text string, args ...interface{}) { if writer != nil { fmt.Fprintf(writer, time.Now().Format("[2006-01-02 15:04:05] ")) fmt.Fprintf(writer, text+"\n", args...) } } func Print(text ...interface{}) { if writer != nil { fmt.Fprintf(writer, time.Now().Format("[2006-01-02 15:04:05] ")) fmt.Fprintln(writer, text...) } } func PrintStack() { if writer != nil { data := debug.Stack() writer.Write(data) } } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/mauview-test/main.go000066400000000000000000000035551460551007700242600ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2019 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package main import ( "go.mau.fi/tcell" "go.mau.fi/mauview" ) type Text struct { mauview.SimpleEventHandler Text string } func (text *Text) Draw(screen mauview.Screen) { for i, char := range text.Text { screen.SetCell(i, 0, tcell.StyleDefault, char) } } func main() { app := mauview.NewApplication() grid := mauview.NewGrid() textComp := &Text{mauview.SimpleEventHandler{}, "Hello, World!"} textComp.OnKey = func(event mauview.KeyEvent) bool { if event.Key() == tcell.KeyCtrlC { app.Stop() } return false } grid.SetColumn(0, 25) grid.SetRow(1, 15) grid.SetRow(3, 5) grid.SetRow(4, 3) grid.AddComponent(mauview.NewBox(textComp), 1, 0, 2, 2) grid.AddComponent(mauview.NewBox(mauview.NewFlex().SetDirection(mauview.FlexRow). AddFixedComponent(mauview.NewBox(nil), 10). AddProportionalComponent(mauview.NewBox(nil), 3). AddProportionalComponent(mauview.NewBox(nil), 1). AddFixedComponent(mauview.NewBox(nil), 10)), 0, 0, 1, 3) grid.AddComponent(mauview.NewBox( mauview.NewGrid(). AddComponent(&Text{mauview.SimpleEventHandler{}, "Hello, World! (again)"}, 0, 1, 1, 1). AddComponent(mauview.NewBox(mauview.NewInputArea().SetPlaceholder("I'm holding a place!")), 0, 0, 2, 1). AddComponent(mauview.NewBox(nil), 1, 1, 1, 1)), 1, 2, 1, 1) grid.AddComponent(mauview.NewBox(mauview.Center(mauview.NewBox(nil), 10, 5).SetAlwaysFocusChild(true)), 2, 2, 1, 1) grid.AddComponent(mauview.NewBox(nil), 0, 4, 2, 1) grid.AddComponent(mauview.NewBox(mauview.NewInputField()), 0, 3, 3, 1) app.SetRoot(mauview.NewBox(grid)) err := app.Start() if err != nil { panic(err) } } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/progress.go000066400000000000000000000042141460551007700225370ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2020 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package mauview import ( "math" "sync/atomic" "time" "go.mau.fi/tcell" ) type ProgressBar struct { *SimpleEventHandler style tcell.Style progress int32 max int indeterminate bool indeterminateStart time.Time } var _ Component = &ProgressBar{} func NewProgressBar() *ProgressBar { return &ProgressBar{ SimpleEventHandler: &SimpleEventHandler{}, style: tcell.StyleDefault, progress: 0, max: 100, indeterminate: true, } } var Blocks = [9]rune{' ', '▏', '▎', '▍', '▌', '▋', '▊', '▉', '█'} func min(a, b int) int { if a < b { return a } return b } func (pb *ProgressBar) SetProgress(progress int) *ProgressBar { pb.progress = int32(min(progress, pb.max)) return pb } func (pb *ProgressBar) Increment(increment int) *ProgressBar { atomic.AddInt32(&pb.progress, int32(increment)) return pb } func (pb *ProgressBar) SetIndeterminate(indeterminate bool) *ProgressBar { pb.indeterminate = indeterminate pb.indeterminateStart = time.Now() return pb } func (pb *ProgressBar) SetMax(max int) *ProgressBar { pb.max = max pb.progress = int32(min(pb.max, int(pb.progress))) return pb } // Draw draws this primitive onto the screen. func (pb *ProgressBar) Draw(screen Screen) { width, _ := screen.Size() if pb.indeterminate { barWidth := width / 6 pos := int(time.Now().Sub(pb.indeterminateStart).Milliseconds()/200) % (width + barWidth) for x := pos - barWidth; x < pos; x++ { screen.SetCell(x, 0, pb.style, Blocks[8]) } } else { progress := math.Min(float64(pb.progress), float64(pb.max)) floatingBlocks := progress * (float64(width) / float64(pb.max)) parts := int(math.Floor(math.Mod(floatingBlocks, 1) * 8)) blocks := int(math.Floor(floatingBlocks)) for x := 0; x < blocks; x++ { screen.SetCell(x, 0, pb.style, Blocks[8]) } screen.SetCell(blocks, 0, pb.style, Blocks[parts]) } } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/screen.go000066400000000000000000000075211460551007700221560ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2019 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package mauview import ( "go.mau.fi/tcell" ) // Screen is a subset of the tcell Screen. // See https://godoc.org/maunium.net/go/tcell#Screen for documentation. type Screen interface { Clear() Fill(rune, tcell.Style) SetStyle(style tcell.Style) SetCell(x, y int, style tcell.Style, ch ...rune) GetContent(x, y int) (mainc rune, combc []rune, style tcell.Style, width int) SetContent(x int, y int, mainc rune, combc []rune, style tcell.Style) ShowCursor(x int, y int) HideCursor() Size() (int, int) Colors() int CharacterSet() string CanDisplay(r rune, checkFallbacks bool) bool HasKey(tcell.Key) bool } // ProxyScreen is a proxy to a tcell Screen with a specific allowed drawing area. type ProxyScreen struct { Parent Screen OffsetX, OffsetY int Width, Height int Style tcell.Style } func NewProxyScreen(parent Screen, offsetX, offsetY, width, height int) Screen { return &ProxyScreen{ Parent: parent, OffsetX: offsetX, OffsetY: offsetY, Width: width, Height: height, Style: tcell.StyleDefault, } } func (ss *ProxyScreen) IsInArea(x, y int) bool { return x >= ss.OffsetX && x <= ss.OffsetX+ss.Width && y >= ss.OffsetY && y <= ss.OffsetY+ss.Height } func (ss *ProxyScreen) YEnd() int { return ss.OffsetY + ss.Height } func (ss *ProxyScreen) XEnd() int { return ss.OffsetX + ss.Width } func (ss *ProxyScreen) OffsetMouseEvent(event MouseEvent) MouseEvent { return OffsetMouseEvent(event, -ss.OffsetX, -ss.OffsetY) } func (ss *ProxyScreen) Clear() { ss.Fill(' ', ss.Style) } func (ss *ProxyScreen) Fill(r rune, style tcell.Style) { for x := ss.OffsetX; x < ss.XEnd(); x++ { for y := ss.OffsetY; y < ss.YEnd(); y++ { ss.Parent.SetCell(x, y, style, r) } } } func (ss *ProxyScreen) SetStyle(style tcell.Style) { ss.Style = style } func (ss *ProxyScreen) adjustCoordinates(x, y int) (int, int, bool) { if x < 0 || y < 0 || (ss.Width >= 0 && x >= ss.Width) || (ss.Height >= 0 && y >= ss.Height) { return -1, -1, false } x += ss.OffsetX y += ss.OffsetY return x, y, true } func (ss *ProxyScreen) SetCell(x, y int, style tcell.Style, ch ...rune) { x, y, ok := ss.adjustCoordinates(x, y) if ok { ss.Parent.SetCell(x, y, style, ch...) } } func (ss *ProxyScreen) GetContent(x, y int) (mainc rune, combc []rune, style tcell.Style, width int) { x, y, ok := ss.adjustCoordinates(x, y) if ok { return ss.Parent.GetContent(x, y) } return 0, nil, tcell.StyleDefault, 0 } func (ss *ProxyScreen) SetContent(x int, y int, mainc rune, combc []rune, style tcell.Style) { x, y, ok := ss.adjustCoordinates(x, y) if ok { ss.Parent.SetContent(x, y, mainc, combc, style) } } func (ss *ProxyScreen) ShowCursor(x, y int) { x, y, ok := ss.adjustCoordinates(x, y) if ok { ss.Parent.ShowCursor(x, y) } } func (ss *ProxyScreen) HideCursor() { ss.Parent.HideCursor() } // Size returns the size of this subscreen. // // If the subscreen doesn't fit in the parent with the set offset and size, // the returned size is whatever can actually be rendered. func (ss *ProxyScreen) Size() (width int, height int) { width, height = ss.Parent.Size() width -= ss.OffsetX height -= ss.OffsetY if width > ss.Width { width = ss.Width } if height > ss.Height { height = ss.Height } return } func (ss *ProxyScreen) Colors() int { return ss.Parent.Colors() } func (ss *ProxyScreen) CharacterSet() string { return ss.Parent.CharacterSet() } func (ss *ProxyScreen) CanDisplay(r rune, checkFallbacks bool) bool { return ss.Parent.CanDisplay(r, checkFallbacks) } func (ss *ProxyScreen) HasKey(key tcell.Key) bool { return ss.Parent.HasKey(key) } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/semigraphics.go000066400000000000000000000442101460551007700233510ustar00rootroot00000000000000// From https://github.com/rivo/tview/blob/master/semigraphics.go // Copyright (c) 2018 Oliver Kuederle // MIT license package mauview import "go.mau.fi/tcell" // Semigraphics provides an easy way to access unicode characters for drawing. // // Named like the unicode characters, 'Semigraphics'-prefix used if unicode block // isn't prefixed itself. const ( // Block: General Punctation U+2000-U+206F (http://unicode.org/charts/PDF/U2000.pdf) SemigraphicsHorizontalEllipsis rune = '\u2026' // … // Block: Box Drawing U+2500-U+257F (http://unicode.org/charts/PDF/U2500.pdf) BoxDrawingsLightHorizontal rune = '\u2500' // ─ BoxDrawingsHeavyHorizontal rune = '\u2501' // ━ BoxDrawingsLightVertical rune = '\u2502' // │ BoxDrawingsHeavyVertical rune = '\u2503' // ┃ BoxDrawingsLightTripleDashHorizontal rune = '\u2504' // ┄ BoxDrawingsHeavyTripleDashHorizontal rune = '\u2505' // ┅ BoxDrawingsLightTripleDashVertical rune = '\u2506' // ┆ BoxDrawingsHeavyTripleDashVertical rune = '\u2507' // ┇ BoxDrawingsLightQuadrupleDashHorizontal rune = '\u2508' // ┈ BoxDrawingsHeavyQuadrupleDashHorizontal rune = '\u2509' // ┉ BoxDrawingsLightQuadrupleDashVertical rune = '\u250a' // ┊ BoxDrawingsHeavyQuadrupleDashVertical rune = '\u250b' // ┋ BoxDrawingsLightDownAndRight rune = '\u250c' // ┌ BoxDrawingsDownLighAndRightHeavy rune = '\u250d' // ┍ BoxDrawingsDownHeavyAndRightLight rune = '\u250e' // ┎ BoxDrawingsHeavyDownAndRight rune = '\u250f' // ┏ BoxDrawingsLightDownAndLeft rune = '\u2510' // ┐ BoxDrawingsDownLighAndLeftHeavy rune = '\u2511' // ┑ BoxDrawingsDownHeavyAndLeftLight rune = '\u2512' // ┒ BoxDrawingsHeavyDownAndLeft rune = '\u2513' // ┓ BoxDrawingsLightUpAndRight rune = '\u2514' // └ BoxDrawingsUpLightAndRightHeavy rune = '\u2515' // ┕ BoxDrawingsUpHeavyAndRightLight rune = '\u2516' // ┖ BoxDrawingsHeavyUpAndRight rune = '\u2517' // ┗ BoxDrawingsLightUpAndLeft rune = '\u2518' // ┘ BoxDrawingsUpLightAndLeftHeavy rune = '\u2519' // ┙ BoxDrawingsUpHeavyAndLeftLight rune = '\u251a' // ┚ BoxDrawingsHeavyUpAndLeft rune = '\u251b' // ┛ BoxDrawingsLightVerticalAndRight rune = '\u251c' // ├ BoxDrawingsVerticalLightAndRightHeavy rune = '\u251d' // ┝ BoxDrawingsUpHeavyAndRightDownLight rune = '\u251e' // ┞ BoxDrawingsDownHeacyAndRightUpLight rune = '\u251f' // ┟ BoxDrawingsVerticalHeavyAndRightLight rune = '\u2520' // ┠ BoxDrawingsDownLightAnbdRightUpHeavy rune = '\u2521' // ┡ BoxDrawingsUpLightAndRightDownHeavy rune = '\u2522' // ┢ BoxDrawingsHeavyVerticalAndRight rune = '\u2523' // ┣ BoxDrawingsLightVerticalAndLeft rune = '\u2524' // ┤ BoxDrawingsVerticalLightAndLeftHeavy rune = '\u2525' // ┥ BoxDrawingsUpHeavyAndLeftDownLight rune = '\u2526' // ┦ BoxDrawingsDownHeavyAndLeftUpLight rune = '\u2527' // ┧ BoxDrawingsVerticalheavyAndLeftLight rune = '\u2528' // ┨ BoxDrawingsDownLightAndLeftUpHeavy rune = '\u2529' // ┨ BoxDrawingsUpLightAndLeftDownHeavy rune = '\u252a' // ┪ BoxDrawingsHeavyVerticalAndLeft rune = '\u252b' // ┫ BoxDrawingsLightDownAndHorizontal rune = '\u252c' // ┬ BoxDrawingsLeftHeavyAndRightDownLight rune = '\u252d' // ┭ BoxDrawingsRightHeavyAndLeftDownLight rune = '\u252e' // ┮ BoxDrawingsDownLightAndHorizontalHeavy rune = '\u252f' // ┯ BoxDrawingsDownHeavyAndHorizontalLight rune = '\u2530' // ┰ BoxDrawingsRightLightAndLeftDownHeavy rune = '\u2531' // ┱ BoxDrawingsLeftLightAndRightDownHeavy rune = '\u2532' // ┲ BoxDrawingsHeavyDownAndHorizontal rune = '\u2533' // ┳ BoxDrawingsLightUpAndHorizontal rune = '\u2534' // ┴ BoxDrawingsLeftHeavyAndRightUpLight rune = '\u2535' // ┵ BoxDrawingsRightHeavyAndLeftUpLight rune = '\u2536' // ┶ BoxDrawingsUpLightAndHorizontalHeavy rune = '\u2537' // ┷ BoxDrawingsUpHeavyAndHorizontalLight rune = '\u2538' // ┸ BoxDrawingsRightLightAndLeftUpHeavy rune = '\u2539' // ┹ BoxDrawingsLeftLightAndRightUpHeavy rune = '\u253a' // ┺ BoxDrawingsHeavyUpAndHorizontal rune = '\u253b' // ┻ BoxDrawingsLightVerticalAndHorizontal rune = '\u253c' // ┼ BoxDrawingsLeftHeavyAndRightVerticalLight rune = '\u253d' // ┽ BoxDrawingsRightHeavyAndLeftVerticalLight rune = '\u253e' // ┾ BoxDrawingsVerticalLightAndHorizontalHeavy rune = '\u253f' // ┿ BoxDrawingsUpHeavyAndDownHorizontalLight rune = '\u2540' // ╀ BoxDrawingsDownHeavyAndUpHorizontalLight rune = '\u2541' // ╁ BoxDrawingsVerticalHeavyAndHorizontalLight rune = '\u2542' // ╂ BoxDrawingsLeftUpHeavyAndRightDownLight rune = '\u2543' // ╃ BoxDrawingsRightUpHeavyAndLeftDownLight rune = '\u2544' // ╄ BoxDrawingsLeftDownHeavyAndRightUpLight rune = '\u2545' // ╅ BoxDrawingsRightDownHeavyAndLeftUpLight rune = '\u2546' // ╆ BoxDrawingsDownLightAndUpHorizontalHeavy rune = '\u2547' // ╇ BoxDrawingsUpLightAndDownHorizontalHeavy rune = '\u2548' // ╈ BoxDrawingsRightLightAndLeftVerticalHeavy rune = '\u2549' // ╉ BoxDrawingsLeftLightAndRightVerticalHeavy rune = '\u254a' // ╊ BoxDrawingsHeavyVerticalAndHorizontal rune = '\u254b' // ╋ BoxDrawingsLightDoubleDashHorizontal rune = '\u254c' // ╌ BoxDrawingsHeavyDoubleDashHorizontal rune = '\u254d' // ╍ BoxDrawingsLightDoubleDashVertical rune = '\u254e' // ╎ BoxDrawingsHeavyDoubleDashVertical rune = '\u254f' // ╏ BoxDrawingsDoubleHorizontal rune = '\u2550' // ═ BoxDrawingsDoubleVertical rune = '\u2551' // ║ BoxDrawingsDownSingleAndRightDouble rune = '\u2552' // ╒ BoxDrawingsDownDoubleAndRightSingle rune = '\u2553' // ╓ BoxDrawingsDoubleDownAndRight rune = '\u2554' // ╔ BoxDrawingsDownSingleAndLeftDouble rune = '\u2555' // ╕ BoxDrawingsDownDoubleAndLeftSingle rune = '\u2556' // ╖ BoxDrawingsDoubleDownAndLeft rune = '\u2557' // ╗ BoxDrawingsUpSingleAndRightDouble rune = '\u2558' // ╘ BoxDrawingsUpDoubleAndRightSingle rune = '\u2559' // ╙ BoxDrawingsDoubleUpAndRight rune = '\u255a' // ╚ BoxDrawingsUpSingleAndLeftDouble rune = '\u255b' // ╛ BoxDrawingsUpDobuleAndLeftSingle rune = '\u255c' // ╜ BoxDrawingsDoubleUpAndLeft rune = '\u255d' // ╝ BoxDrawingsVerticalSingleAndRightDouble rune = '\u255e' // ╞ BoxDrawingsVerticalDoubleAndRightSingle rune = '\u255f' // ╟ BoxDrawingsDoubleVerticalAndRight rune = '\u2560' // ╠ BoxDrawingsVerticalSingleAndLeftDouble rune = '\u2561' // ╡ BoxDrawingsVerticalDoubleAndLeftSingle rune = '\u2562' // ╢ BoxDrawingsDoubleVerticalAndLeft rune = '\u2563' // ╣ BoxDrawingsDownSingleAndHorizontalDouble rune = '\u2564' // ╤ BoxDrawingsDownDoubleAndHorizontalSingle rune = '\u2565' // ╥ BoxDrawingsDoubleDownAndHorizontal rune = '\u2566' // ╦ BoxDrawingsUpSingleAndHorizontalDouble rune = '\u2567' // ╧ BoxDrawingsUpDoubleAndHorizontalSingle rune = '\u2568' // ╨ BoxDrawingsDoubleUpAndHorizontal rune = '\u2569' // ╩ BoxDrawingsVerticalSingleAndHorizontalDouble rune = '\u256a' // ╪ BoxDrawingsVerticalDoubleAndHorizontalSingle rune = '\u256b' // ╫ BoxDrawingsDoubleVerticalAndHorizontal rune = '\u256c' // ╬ BoxDrawingsLightArcDownAndRight rune = '\u256d' // ╭ BoxDrawingsLightArcDownAndLeft rune = '\u256e' // ╮ BoxDrawingsLightArcUpAndLeft rune = '\u256f' // ╯ BoxDrawingsLightArcUpAndRight rune = '\u2570' // ╰ BoxDrawingsLightDiagonalUpperRightToLowerLeft rune = '\u2571' // ╱ BoxDrawingsLightDiagonalUpperLeftToLowerRight rune = '\u2572' // ╲ BoxDrawingsLightDiagonalCross rune = '\u2573' // ╳ BoxDrawingsLightLeft rune = '\u2574' // ╴ BoxDrawingsLightUp rune = '\u2575' // ╵ BoxDrawingsLightRight rune = '\u2576' // ╶ BoxDrawingsLightDown rune = '\u2577' // ╷ BoxDrawingsHeavyLeft rune = '\u2578' // ╸ BoxDrawingsHeavyUp rune = '\u2579' // ╹ BoxDrawingsHeavyRight rune = '\u257a' // ╺ BoxDrawingsHeavyDown rune = '\u257b' // ╻ BoxDrawingsLightLeftAndHeavyRight rune = '\u257c' // ╼ BoxDrawingsLightUpAndHeavyDown rune = '\u257d' // ╽ BoxDrawingsHeavyLeftAndLightRight rune = '\u257e' // ╾ BoxDrawingsHeavyUpAndLightDown rune = '\u257f' // ╿ ) // SemigraphicJoints is a map for joining semigraphic (or otherwise) runes. // So far only light lines are supported but if you want to change the border // styling you need to provide the joints, too. // The matching will be sorted ascending by rune value, so you don't need to // provide all rune combinations, // e.g. (─) + (│) = (┼) will also match (│) + (─) = (┼) var SemigraphicJoints = map[string]rune{ // (─) + (│) = (┼) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVertical}): BoxDrawingsLightVerticalAndHorizontal, // (─) + (┌) = (┬) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndRight}): BoxDrawingsLightDownAndHorizontal, // (─) + (┐) = (┬) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightDownAndHorizontal, // (─) + (└) = (┴) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndRight}): BoxDrawingsLightUpAndHorizontal, // (─) + (┘) = (┴) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightUpAndHorizontal, // (─) + (├) = (┼) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal, // (─) + (┤) = (┼) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal, // (─) + (┬) = (┬) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal, // (─) + (┴) = (┴) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal, // (─) + (┼) = (┼) string([]rune{BoxDrawingsLightHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (│) + (┌) = (├) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndRight}): BoxDrawingsLightVerticalAndRight, // (│) + (┐) = (┤) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightVerticalAndLeft, // (│) + (└) = (├) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndRight, // (│) + (┘) = (┤) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndLeft, // (│) + (├) = (├) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight, // (│) + (┤) = (┤) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft, // (│) + (┬) = (┼) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (│) + (┴) = (┼) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (│) + (┼) = (┼) string([]rune{BoxDrawingsLightVertical, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┌) + (┐) = (┬) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightDownAndLeft}): BoxDrawingsLightDownAndHorizontal, // (┌) + (└) = (├) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndRight, // (┌) + (┘) = (┼) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndHorizontal, // (┌) + (├) = (├) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight, // (┌) + (┤) = (┼) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal, // (┌) + (┬) = (┬) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal, // (┌) + (┴) = (┼) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┌) + (┴) = (┼) string([]rune{BoxDrawingsLightDownAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┐) + (└) = (┼) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndRight}): BoxDrawingsLightVerticalAndHorizontal, // (┐) + (┘) = (┤) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightVerticalAndLeft, // (┐) + (├) = (┼) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal, // (┐) + (┤) = (┤) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft, // (┐) + (┬) = (┬) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightDownAndHorizontal, // (┐) + (┴) = (┼) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┐) + (┼) = (┼) string([]rune{BoxDrawingsLightDownAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (└) + (┘) = (┴) string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightUpAndLeft}): BoxDrawingsLightUpAndHorizontal, // (└) + (├) = (├) string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndRight, // (└) + (┤) = (┼) string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal, // (└) + (┬) = (┼) string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (└) + (┴) = (┴) string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal, // (└) + (┼) = (┼) string([]rune{BoxDrawingsLightUpAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┘) + (├) = (┼) string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndRight}): BoxDrawingsLightVerticalAndHorizontal, // (┘) + (┤) = (┤) string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndLeft, // (┘) + (┬) = (┼) string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┘) + (┴) = (┴) string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightUpAndHorizontal, // (┘) + (┼) = (┼) string([]rune{BoxDrawingsLightUpAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (├) + (┤) = (┼) string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightVerticalAndLeft}): BoxDrawingsLightVerticalAndHorizontal, // (├) + (┬) = (┼) string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (├) + (┴) = (┼) string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (├) + (┼) = (┼) string([]rune{BoxDrawingsLightVerticalAndRight, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┤) + (┬) = (┼) string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightDownAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┤) + (┴) = (┼) string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┤) + (┼) = (┼) string([]rune{BoxDrawingsLightVerticalAndLeft, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┬) + (┴) = (┼) string([]rune{BoxDrawingsLightDownAndHorizontal, BoxDrawingsLightUpAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┬) + (┼) = (┼) string([]rune{BoxDrawingsLightDownAndHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, // (┴) + (┼) = (┼) string([]rune{BoxDrawingsLightUpAndHorizontal, BoxDrawingsLightVerticalAndHorizontal}): BoxDrawingsLightVerticalAndHorizontal, } // PrintJoinedSemigraphics prints a semigraphics rune into the screen at the given // position with the given color, joining it with any existing semigraphics // rune. Background colors are preserved. At this point, only regular single // line borders are supported. func PrintJoinedSemigraphics(screen tcell.Screen, x, y int, ch rune, color tcell.Color) { previous, _, style, _ := screen.GetContent(x, y) style = style.Foreground(color) // What's the resulting rune? var result rune if ch == previous { result = ch } else { if ch < previous { previous, ch = ch, previous } result = SemigraphicJoints[string([]rune{previous, ch})] } if result == 0 { result = ch } // We only print something if we have something. screen.SetContent(x, y, result, nil, style) } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/styles.go000066400000000000000000000034211460551007700222150ustar00rootroot00000000000000// From https://github.com/rivo/tview/blob/master/styles.go // Copyright (c) 2018 Oliver Kuederle // MIT license package mauview import "go.mau.fi/tcell" // Styles defines various colors used when primitives are initialized. These // may be changed to accommodate a different look and feel. // // The default is for applications with a black background and basic colors: // black, white, yellow, green, and blue. var Styles = struct { PrimitiveBackgroundColor tcell.Color // Main background color for primitives. ContrastBackgroundColor tcell.Color // Background color for contrasting elements. MoreContrastBackgroundColor tcell.Color // Background color for even more contrasting elements. BorderColor tcell.Color // Box borders. TitleColor tcell.Color // Box titles. GraphicsColor tcell.Color // Graphics. PrimaryTextColor tcell.Color // Primary text. SecondaryTextColor tcell.Color // Secondary text (e.g. labels). TertiaryTextColor tcell.Color // Tertiary text (e.g. subtitles, notes). InverseTextColor tcell.Color // Text on primary-colored backgrounds. ContrastSecondaryTextColor tcell.Color // Secondary text on ContrastBackgroundColor-colored backgrounds. }{ PrimitiveBackgroundColor: tcell.ColorBlack, ContrastBackgroundColor: tcell.ColorBlue, MoreContrastBackgroundColor: tcell.ColorGreen, BorderColor: tcell.ColorWhite, TitleColor: tcell.ColorWhite, GraphicsColor: tcell.ColorWhite, PrimaryTextColor: tcell.ColorWhite, SecondaryTextColor: tcell.ColorYellow, TertiaryTextColor: tcell.ColorGreen, InverseTextColor: tcell.ColorBlue, ContrastSecondaryTextColor: tcell.ColorDarkCyan, } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/textfield.go000066400000000000000000000025001460551007700226570ustar00rootroot00000000000000// mauview - A Go TUI library based on tcell. // Copyright © 2019 Tulir Asokan // // This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at http://mozilla.org/MPL/2.0/. package mauview import ( "sync" "go.mau.fi/tcell" ) type TextField struct { sync.Mutex *SimpleEventHandler text string style tcell.Style } func NewTextField() *TextField { return &TextField{ SimpleEventHandler: &SimpleEventHandler{}, text: "", style: tcell.StyleDefault.Foreground(Styles.PrimaryTextColor), } } func (tf *TextField) SetText(text string) *TextField { tf.Lock() tf.text = text tf.Unlock() return tf } func (tf *TextField) SetTextColor(color tcell.Color) *TextField { tf.Lock() tf.style = tf.style.Foreground(color) tf.Unlock() return tf } func (tf *TextField) SetBackgroundColor(color tcell.Color) *TextField { tf.Lock() tf.style = tf.style.Background(color) tf.Unlock() return tf } func (tf *TextField) SetStyle(style tcell.Style) *TextField { tf.Lock() tf.style = style tf.Unlock() return tf } func (tf *TextField) Draw(screen Screen) { tf.Lock() width, _ := screen.Size() screen.SetStyle(tf.style) screen.Clear() PrintWithStyle(screen, tf.text, 0, 0, width, AlignLeft, tf.style) tf.Unlock() } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/textview.go000066400000000000000000000705511460551007700225610ustar00rootroot00000000000000// From https://github.com/rivo/tview/blob/master/textview.go package mauview import ( "bytes" "fmt" "regexp" "sync" "unicode/utf8" "github.com/mattn/go-runewidth" "go.mau.fi/tcell" ) // TabSize is the number of spaces with which a tab character will be replaced. var TabSize = 4 // textViewIndex contains information about each line displayed in the text // view. type textViewIndex struct { Line int // The index into the "buffer" variable. Pos int // The index into the "buffer" string (byte position). NextPos int // The (byte) index of the next character in this buffer line. Width int // The screen width of this line. ForegroundColor string // The starting foreground color ("" = don't change, "-" = reset). BackgroundColor string // The starting background color ("" = don't change, "-" = reset). Attributes string // The starting attributes ("" = don't change, "-" = reset). Region string // The starting region ID. } // TextView is a box which displays text. It implements the io.Writer interface // so you can stream text to it. This does not trigger a redraw automatically // but if a handler is installed via SetChangedFunc(), you can cause it to be // redrawn. (See SetChangedFunc() for more details.) // // # Navigation // // If the text view is scrollable (the default), text is kept in a buffer which // may be larger than the screen and can be navigated similarly to Vim: // // - h, left arrow: Move left. // - l, right arrow: Move right. // - j, down arrow: Move down. // - k, up arrow: Move up. // - g, home: Move to the top. // - G, end: Move to the bottom. // - Ctrl-F, page down: Move down by one page. // - Ctrl-B, page up: Move up by one page. // // If the text is not scrollable, any text above the top visible line is // discarded. // // Use SetInputCapture() to override or modify keyboard input. // // # Colors // // If dynamic colors are enabled via SetDynamicColors(), text color can be // changed dynamically by embedding color strings in square brackets. This works // the same way as anywhere else. Please see the package documentation for more // information. // // # Regions and Highlights // // If regions are enabled via SetRegions(), you can define text regions within // the text and assign region IDs to them. Text regions start with region tags. // Region tags are square brackets that contain a region ID in double quotes, // for example: // // We define a ["rg"]region[""] here. // // A text region ends with the next region tag. Tags with no region ID ([""]) // don't start new regions. They can therefore be used to mark the end of a // region. Region IDs must satisfy the following regular expression: // // [a-zA-Z0-9_,;: \-\.]+ // // Regions can be highlighted by calling the Highlight() function with one or // more region IDs. This can be used to display search results, for example. // // The ScrollToHighlight() function can be used to jump to the currently // highlighted region once when the text view is drawn the next time. // // See https://github.com/rivo/tview/wiki/TextView for an example. type TextView struct { sync.Mutex // The text buffer. buffer []string // The last bytes that have been received but are not part of the buffer yet. recentBytes []byte // The processed line index. This is nil if the buffer has changed and needs // to be re-indexed. index []*textViewIndex // The text alignment, one of AlignLeft, AlignCenter, or AlignRight. align int // Indices into the "index" slice which correspond to the first line of the // first highlight and the last line of the last highlight. This is calculated // during re-indexing. Set to -1 if there is no current highlight. fromHighlight, toHighlight int // The screen space column of the highlight in its first line. Set to -1 if // there is no current highlight. posHighlight int // A set of region IDs that are currently highlighted. highlights map[string]struct{} // The last width for which the current table is drawn. lastWidth int // The screen width of the longest line in the index (not the buffer). longestLine int // The index of the first line shown in the text view. lineOffset int // If set to true, the text view will always remain at the end of the content. trackEnd bool // The number of characters to be skipped on each line (not in wrap mode). columnOffset int // The height of the content the last time the text view was drawn. pageSize int // If set to true, the text view will keep a buffer of text which can be // navigated when the text is longer than what fits into the box. scrollable bool // If set to true, lines that are longer than the available width are wrapped // onto the next line. If set to false, any characters beyond the available // width are discarded. wrap bool // If set to true and if wrap is also true, lines are split at spaces or // after punctuation characters. wordWrap bool baseStyle tcell.Style // If set to true, the text color can be changed dynamically by piping color // strings in square brackets to the text view. dynamicColors bool // If set to true, region tags can be used to define regions. regions bool // A temporary flag which, when true, will automatically bring the current // highlight(s) into the visible screen. scrollToHighlights bool // An optional function which is called when the content of the text view has // changed. changed func() // An optional function which is called when the user presses one of the // following keys: Escape, Enter, Tab, Backtab. done func(tcell.Key) } // NewTextView returns a new text view. func NewTextView() *TextView { return &TextView{ highlights: make(map[string]struct{}), lineOffset: -1, scrollable: true, align: AlignLeft, wrap: true, baseStyle: tcell.StyleDefault.Foreground(Styles.PrimaryTextColor), regions: false, dynamicColors: false, } } // SetScrollable sets the flag that decides whether or not the text view is // scrollable. If true, text is kept in a buffer and can be navigated. func (t *TextView) SetScrollable(scrollable bool) *TextView { t.scrollable = scrollable if !scrollable { t.trackEnd = true } return t } // SetWrap sets the flag that, if true, leads to lines that are longer than the // available width being wrapped onto the next line. If false, any characters // beyond the available width are not displayed. func (t *TextView) SetWrap(wrap bool) *TextView { if t.wrap != wrap { t.index = nil } t.wrap = wrap return t } // SetWordWrap sets the flag that, if true and if the "wrap" flag is also true // (see SetWrap()), wraps the line at spaces or after punctuation marks. Note // that trailing spaces will not be printed. // // This flag is ignored if the "wrap" flag is false. func (t *TextView) SetWordWrap(wrapOnWords bool) *TextView { if t.wordWrap != wrapOnWords { t.index = nil } t.wordWrap = wrapOnWords return t } // SetTextAlign sets the text alignment within the text view. This must be // either AlignLeft, AlignCenter, or AlignRight. func (t *TextView) SetTextAlign(align int) *TextView { if t.align != align { t.index = nil } t.align = align return t } // SetTextColor sets the initial color of the text (which can be changed // dynamically by sending color strings in square brackets to the text view if // dynamic colors are enabled). func (t *TextView) SetTextColor(color tcell.Color) *TextView { t.baseStyle = t.baseStyle.Foreground(color) return t } func (t *TextView) SetBackgroundColor(color tcell.Color) *TextView { t.baseStyle = t.baseStyle.Background(color) return t } // SetText sets the text of this text view to the provided string. Previously // contained text will be removed. func (t *TextView) SetText(text string) *TextView { t.Clear() fmt.Fprint(t, text) return t } // SetDynamicColors sets the flag that allows the text color to be changed // dynamically. See class description for details. func (t *TextView) SetDynamicColors(dynamic bool) *TextView { if t.dynamicColors != dynamic { t.index = nil } t.dynamicColors = dynamic return t } // SetRegions sets the flag that allows to define regions in the text. See class // description for details. func (t *TextView) SetRegions(regions bool) *TextView { if t.regions != regions { t.index = nil } t.regions = regions return t } // SetChangedFunc sets a handler function which is called when the text of the // text view has changed. This is useful when text is written to this io.Writer // in a separate goroutine. This does not automatically cause the screen to be // refreshed so you may want to use the "changed" handler to redraw the screen. // // Note that to avoid race conditions or deadlocks, there are a few rules you // should follow: // // - You can call Application.Draw() from this handler. // - You can call TextView.HasFocus() from this handler. // - During the execution of this handler, access to any other variables from // this primitive or any other primitive should be queued using // Application.QueueUpdate(). // // See package description for details on dealing with concurrency. func (t *TextView) SetChangedFunc(handler func()) *TextView { t.changed = handler return t } // SetDoneFunc sets a handler which is called when the user presses on the // following keys: Escape, Enter, Tab, Backtab. The key is passed to the // handler. func (t *TextView) SetDoneFunc(handler func(key tcell.Key)) *TextView { t.done = handler return t } // ScrollTo scrolls to the specified row and column (both starting with 0). func (t *TextView) ScrollTo(row, column int) *TextView { if !t.scrollable { return t } t.lineOffset = row t.columnOffset = column return t } // ScrollToBeginning scrolls to the top left corner of the text if the text view // is scrollable. func (t *TextView) ScrollToBeginning() *TextView { if !t.scrollable { return t } t.trackEnd = false t.lineOffset = 0 t.columnOffset = 0 return t } // ScrollToEnd scrolls to the bottom left corner of the text if the text view // is scrollable. Adding new rows to the end of the text view will cause it to // scroll with the new data. func (t *TextView) ScrollToEnd() *TextView { if !t.scrollable { return t } t.trackEnd = true t.columnOffset = 0 return t } // GetScrollOffset returns the number of rows and columns that are skipped at // the top left corner when the text view has been scrolled. func (t *TextView) GetScrollOffset() (row, column int) { return t.lineOffset, t.columnOffset } // Clear removes all text from the buffer. func (t *TextView) Clear() *TextView { t.buffer = nil t.recentBytes = nil t.index = nil return t } // Highlight specifies which regions should be highlighted. See class // description for details on regions. Empty region strings are ignored. // // Text in highlighted regions will be drawn inverted, i.e. with their // background and foreground colors swapped. // // Calling this function will remove any previous highlights. To remove all // highlights, call this function without any arguments. func (t *TextView) Highlight(regionIDs ...string) *TextView { t.highlights = make(map[string]struct{}) for _, id := range regionIDs { if id == "" { continue } t.highlights[id] = struct{}{} } t.index = nil return t } // GetHighlights returns the IDs of all currently highlighted regions. func (t *TextView) GetHighlights() (regionIDs []string) { for id := range t.highlights { regionIDs = append(regionIDs, id) } return } // ScrollToHighlight will cause the visible area to be scrolled so that the // highlighted regions appear in the visible area of the text view. This // repositioning happens the next time the text view is drawn. It happens only // once so you will need to call this function repeatedly to always keep // highlighted regions in view. // // Nothing happens if there are no highlighted regions or if the text view is // not scrollable. func (t *TextView) ScrollToHighlight() *TextView { if len(t.highlights) == 0 || !t.scrollable || !t.regions { return t } t.index = nil t.scrollToHighlights = true t.trackEnd = false return t } // GetRegionText returns the text of the region with the given ID. If dynamic // colors are enabled, color tags are stripped from the text. Newlines are // always returned as '\n' runes. // // If the region does not exist or if regions are turned off, an empty string // is returned. func (t *TextView) GetRegionText(regionID string) string { if !t.regions || regionID == "" { return "" } var ( buffer bytes.Buffer currentRegionID string ) for _, str := range t.buffer { // Find all color tags in this line. var colorTagIndices [][]int if t.dynamicColors { colorTagIndices = colorPattern.FindAllStringIndex(str, -1) } // Find all regions in this line. var ( regionIndices [][]int regions [][]string ) if t.regions { regionIndices = regionPattern.FindAllStringIndex(str, -1) regions = regionPattern.FindAllStringSubmatch(str, -1) } // Analyze this line. var currentTag, currentRegion int for pos, ch := range str { // Skip any color tags. if currentTag < len(colorTagIndices) && pos >= colorTagIndices[currentTag][0] && pos < colorTagIndices[currentTag][1] { if pos == colorTagIndices[currentTag][1]-1 { currentTag++ } continue } // Skip any regions. if currentRegion < len(regionIndices) && pos >= regionIndices[currentRegion][0] && pos < regionIndices[currentRegion][1] { if pos == regionIndices[currentRegion][1]-1 { if currentRegionID == regionID { // This is the end of the requested region. We're done. return buffer.String() } currentRegionID = regions[currentRegion][1] currentRegion++ } continue } // Add this rune. if currentRegionID == regionID { buffer.WriteRune(ch) } } // Add newline. if currentRegionID == regionID { buffer.WriteRune('\n') } } return escapePattern.ReplaceAllString(buffer.String(), `[$1$2]`) } // Write lets us implement the io.Writer interface. Tab characters will be // replaced with TabSize space characters. A "\n" or "\r\n" will be interpreted // as a new line. func (t *TextView) Write(p []byte) (n int, err error) { // Notify at the end. t.Lock() changed := t.changed t.Unlock() if changed != nil { defer changed() // Deadlocks may occur if we lock here. } t.Lock() defer t.Unlock() // Copy data over. newBytes := append(t.recentBytes, p...) t.recentBytes = nil // If we have a trailing invalid UTF-8 byte, we'll wait. if r, _ := utf8.DecodeLastRune(p); r == utf8.RuneError { t.recentBytes = newBytes return len(p), nil } // If we have a trailing open dynamic color, exclude it. if t.dynamicColors { openColor := regexp.MustCompile(`\[([a-zA-Z]*|#[0-9a-zA-Z]*)$`) location := openColor.FindIndex(newBytes) if location != nil { t.recentBytes = newBytes[location[0]:] newBytes = newBytes[:location[0]] } } // If we have a trailing open region, exclude it. if t.regions { openRegion := regexp.MustCompile(`\["[a-zA-Z0-9_,;: \-\.]*"?$`) location := openRegion.FindIndex(newBytes) if location != nil { t.recentBytes = newBytes[location[0]:] newBytes = newBytes[:location[0]] } } // Transform the new bytes into strings. newLine := regexp.MustCompile(`\r?\n`) newBytes = bytes.Replace(newBytes, []byte{'\t'}, bytes.Repeat([]byte{' '}, TabSize), -1) for index, line := range newLine.Split(string(newBytes), -1) { if index == 0 { if len(t.buffer) == 0 { t.buffer = []string{line} } else { t.buffer[len(t.buffer)-1] += line } } else { t.buffer = append(t.buffer, line) } } // Reset the index. t.index = nil return len(p), nil } // reindexBuffer re-indexes the buffer such that we can use it to easily draw // the buffer onto the screen. Each line in the index will contain a pointer // into the buffer from which on we will print text. It will also contain the // color with which the line starts. func (t *TextView) reindexBuffer(width int) { if t.index != nil { return // Nothing has changed. We can still use the current index. } t.index = nil t.fromHighlight, t.toHighlight, t.posHighlight = -1, -1, -1 // If there's no space, there's no index. if width < 1 { return } // Initial states. regionID := "" var highlighted bool // Go through each line in the buffer. for bufferIndex, str := range t.buffer { // Find all color tags in this line. Then remove them. var ( colorTagIndices [][]int colorTags [][]string escapeIndices [][]int ) strippedStr := str if t.dynamicColors { colorTagIndices, colorTags, _, _, escapeIndices, strippedStr, _ = decomposeString(str, true, false) } // Find all regions in this line. Then remove them. var ( regionIndices [][]int regions [][]string ) if t.regions { regionIndices = regionPattern.FindAllStringIndex(str, -1) regions = regionPattern.FindAllStringSubmatch(str, -1) strippedStr = regionPattern.ReplaceAllString(strippedStr, "") } // We don't need the original string anymore for now. str = strippedStr // Split the line if required. var splitLines []string if t.wrap && len(str) > 0 { for len(str) > 0 { extract := runewidth.Truncate(str, width, "") if t.wordWrap && len(extract) < len(str) { // Add any spaces from the next line. if spaces := spacePattern.FindStringIndex(str[len(extract):]); spaces != nil && spaces[0] == 0 { extract = str[:len(extract)+spaces[1]] } // Can we split before the mandatory end? matches := boundaryPattern.FindAllStringIndex(extract, -1) if len(matches) > 0 { // Yes. Let's split there. extract = extract[:matches[len(matches)-1][1]] } } splitLines = append(splitLines, extract) str = str[len(extract):] } } else { // No need to split the line. splitLines = []string{str} } // Create index from split lines. var ( originalPos, colorPos, regionPos, escapePos int foregroundColor, backgroundColor, attributes string ) for _, splitLine := range splitLines { line := &textViewIndex{ Line: bufferIndex, Pos: originalPos, ForegroundColor: foregroundColor, BackgroundColor: backgroundColor, Attributes: attributes, Region: regionID, } // Shift original position with tags. lineLength := len(splitLine) remainingLength := lineLength tagEnd := originalPos totalTagLength := 0 for { // Which tag comes next? nextTag := make([][3]int, 0, 3) if colorPos < len(colorTagIndices) { nextTag = append(nextTag, [3]int{colorTagIndices[colorPos][0], colorTagIndices[colorPos][1], 0}) // 0 = color tag. } if regionPos < len(regionIndices) { nextTag = append(nextTag, [3]int{regionIndices[regionPos][0], regionIndices[regionPos][1], 1}) // 1 = region tag. } if escapePos < len(escapeIndices) { nextTag = append(nextTag, [3]int{escapeIndices[escapePos][0], escapeIndices[escapePos][1], 2}) // 2 = escape tag. } minPos := -1 tagIndex := -1 for index, pair := range nextTag { if minPos < 0 || pair[0] < minPos { minPos = pair[0] tagIndex = index } } // Is the next tag in range? if tagIndex < 0 || minPos >= tagEnd+remainingLength { break // No. We're done with this line. } // Advance. strippedTagStart := nextTag[tagIndex][0] - originalPos - totalTagLength tagEnd = nextTag[tagIndex][1] tagLength := tagEnd - nextTag[tagIndex][0] if nextTag[tagIndex][2] == 2 { tagLength = 1 } totalTagLength += tagLength remainingLength = lineLength - (tagEnd - originalPos - totalTagLength) // Process the tag. switch nextTag[tagIndex][2] { case 0: // Process color tags. foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos]) colorPos++ case 1: // Process region tags. regionID = regions[regionPos][1] _, highlighted = t.highlights[regionID] // Update highlight range. if highlighted { line := len(t.index) if t.fromHighlight < 0 { t.fromHighlight, t.toHighlight = line, line t.posHighlight = runewidth.StringWidth(splitLine[:strippedTagStart]) } else if line > t.toHighlight { t.toHighlight = line } } regionPos++ case 2: // Process escape tags. escapePos++ } } // Advance to next line. originalPos += lineLength + totalTagLength // Append this line. line.NextPos = originalPos line.Width = runewidth.StringWidth(splitLine) t.index = append(t.index, line) } // Word-wrapped lines may have trailing whitespace. Remove it. if t.wrap && t.wordWrap { for _, line := range t.index { str := t.buffer[line.Line][line.Pos:line.NextPos] spaces := spacePattern.FindAllStringIndex(str, -1) if spaces != nil && spaces[len(spaces)-1][1] == len(str) { oldNextPos := line.NextPos line.NextPos -= spaces[len(spaces)-1][1] - spaces[len(spaces)-1][0] line.Width -= runewidth.StringWidth(t.buffer[line.Line][line.NextPos:oldNextPos]) } } } } // Calculate longest line. t.longestLine = 0 for _, line := range t.index { if line.Width > t.longestLine { t.longestLine = line.Width } } } // Draw draws this primitive onto the screen. func (t *TextView) Draw(screen Screen) { t.Lock() defer t.Unlock() screen.SetStyle(t.baseStyle) screen.Clear() // Get the available size. width, height := screen.Size() t.pageSize = height // If the width has changed, we need to reindex. if width != t.lastWidth && t.wrap { t.index = nil } t.lastWidth = width // Re-index. t.reindexBuffer(width) // If we don't have an index, there's nothing to draw. if t.index == nil { return } // Move to highlighted regions. if t.regions && t.scrollToHighlights && t.fromHighlight >= 0 { // Do we fit the entire height? if t.toHighlight-t.fromHighlight+1 < height { // Yes, let's center the highlights. t.lineOffset = (t.fromHighlight + t.toHighlight - height) / 2 } else { // No, let's move to the start of the highlights. t.lineOffset = t.fromHighlight } // If the highlight is too far to the right, move it to the middle. if t.posHighlight-t.columnOffset > 3*width/4 { t.columnOffset = t.posHighlight - width/2 } // If the highlight is off-screen on the left, move it on-screen. if t.posHighlight-t.columnOffset < 0 { t.columnOffset = t.posHighlight - width/4 } } t.scrollToHighlights = false // Adjust line offset. if t.lineOffset+height > len(t.index) { t.trackEnd = true } if t.trackEnd { t.lineOffset = len(t.index) - height } if t.lineOffset < 0 { t.lineOffset = 0 } // Adjust column offset. if t.align == AlignLeft { if t.columnOffset+width > t.longestLine { t.columnOffset = t.longestLine - width } if t.columnOffset < 0 { t.columnOffset = 0 } } else if t.align == AlignRight { if t.columnOffset-width < -t.longestLine { t.columnOffset = width - t.longestLine } if t.columnOffset > 0 { t.columnOffset = 0 } } else { // AlignCenter. half := (t.longestLine - width) / 2 if half > 0 { if t.columnOffset > half { t.columnOffset = half } if t.columnOffset < -half { t.columnOffset = -half } } else { t.columnOffset = 0 } } // Draw the buffer. defaultStyle := t.baseStyle for line := t.lineOffset; line < len(t.index); line++ { // Are we done? if line-t.lineOffset >= height { break } // Get the text for this line. index := t.index[line] text := t.buffer[index.Line][index.Pos:index.NextPos] foregroundColor := index.ForegroundColor backgroundColor := index.BackgroundColor attributes := index.Attributes regionID := index.Region // Get color tags. var ( colorTagIndices [][]int colorTags [][]string escapeIndices [][]int ) strippedText := text if t.dynamicColors { colorTagIndices, colorTags, _, _, escapeIndices, strippedText, _ = decomposeString(text, true, false) } // Get regions. var ( regionIndices [][]int regions [][]string ) if t.regions { regionIndices = regionPattern.FindAllStringIndex(text, -1) regions = regionPattern.FindAllStringSubmatch(text, -1) strippedText = regionPattern.ReplaceAllString(strippedText, "") if !t.dynamicColors { escapeIndices = escapePattern.FindAllStringIndex(text, -1) strippedText = escapePattern.ReplaceAllString(strippedText, "[$1$2]") } } // Calculate the position of the line. var skip, posX int if t.align == AlignLeft { posX = -t.columnOffset } else if t.align == AlignRight { posX = width - index.Width - t.columnOffset } else { // AlignCenter. posX = (width-index.Width)/2 - t.columnOffset } if posX < 0 { skip = -posX posX = 0 } // Print the line. var colorPos, regionPos, escapePos, tagOffset, skipped int iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { // Process tags. for { if colorPos < len(colorTags) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] { // Get the color. foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colorTags[colorPos]) tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0] colorPos++ } else if regionPos < len(regionIndices) && textPos+tagOffset >= regionIndices[regionPos][0] && textPos+tagOffset < regionIndices[regionPos][1] { // Get the region. regionID = regions[regionPos][1] tagOffset += regionIndices[regionPos][1] - regionIndices[regionPos][0] regionPos++ } else { break } } // Skip the second-to-last character of an escape tag. if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 { tagOffset++ escapePos++ } // Mix the existing style with the new style. _, _, existingStyle, _ := screen.GetContent(posX, line-t.lineOffset) _, background, _ := existingStyle.Decompose() style := overlayStyle(defaultStyle.Background(background), foregroundColor, backgroundColor, attributes) // Do we highlight this character? var highlighted bool if len(regionID) > 0 { if _, ok := t.highlights[regionID]; ok { highlighted = true } } if highlighted { _, _, attrs := style.Decompose() reversed := attrs&tcell.AttrReverse != 0 style = style.Reverse(!reversed) } // Skip to the right. if !t.wrap && skipped < skip { skipped += screenWidth return false } // Stop at the right border. if posX+screenWidth > width { return true } // Draw the character. for offset := screenWidth - 1; offset >= 0; offset-- { if offset == 0 { screen.SetContent(posX+offset, line-t.lineOffset, main, comb, style) } else { screen.SetContent(posX+offset, line-t.lineOffset, ' ', nil, style) } } // Advance. posX += screenWidth return false }) } // If this view is not scrollable, we'll purge the buffer of lines that have // scrolled out of view. if !t.scrollable && t.lineOffset > 0 { t.buffer = t.buffer[t.index[t.lineOffset].Line:] t.index = nil } } func (t *TextView) Submit(event KeyEvent) bool { return true } func (t *TextView) OnKeyEvent(event KeyEvent) bool { key := event.Key() if !t.scrollable { return false } switch key { case tcell.KeyRune: switch event.Rune() { case 'g': // Home. t.trackEnd = false t.lineOffset = 0 t.columnOffset = 0 case 'G': // End. t.trackEnd = true t.columnOffset = 0 case 'j': // Down. t.lineOffset++ case 'k': // Up. t.trackEnd = false t.lineOffset-- case 'h': // Left. t.columnOffset-- case 'l': // Right. t.columnOffset++ } case tcell.KeyHome: t.trackEnd = false t.lineOffset = 0 t.columnOffset = 0 case tcell.KeyEnd: t.trackEnd = true t.columnOffset = 0 case tcell.KeyUp: t.trackEnd = false t.lineOffset-- case tcell.KeyDown: t.lineOffset++ case tcell.KeyLeft: t.columnOffset-- case tcell.KeyRight: t.columnOffset++ case tcell.KeyPgDn, tcell.KeyCtrlF: t.lineOffset += t.pageSize case tcell.KeyPgUp, tcell.KeyCtrlB: t.trackEnd = false t.lineOffset -= t.pageSize default: return false } return true } func (t *TextView) OnMouseEvent(event MouseEvent) bool { if !t.scrollable { return false } switch event.Buttons() { case tcell.WheelDown: t.lineOffset += 3 case tcell.WheelUp: t.lineOffset -= 3 default: return false } return true } func (t *TextView) OnPasteEvent(event PasteEvent) bool { return false } golang-go.mau-mauview-0.2.1+git20240409+f020cbb/util.go000066400000000000000000000565171460551007700216650ustar00rootroot00000000000000// From https://github.com/rivo/tview/blob/master/util.go // Copyright (c) 2018 Oliver Kuederle // MIT license package mauview import ( "math" "regexp" "sort" "strconv" "github.com/mattn/go-runewidth" "github.com/rivo/uniseg" "go.mau.fi/tcell" ) // Text alignment within a box. const ( AlignLeft = iota AlignCenter AlignRight ) // Common regular expressions. var ( colorPattern = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([lbidrus]+|\-)?)?)?\]`) regionPattern = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`) escapePattern = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`) nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`) boundaryPattern = regexp.MustCompile(`(([,\.\-:;!\?&#+]|\n)[ \t\f\r]*|([ \t\f\r]+))`) spacePattern = regexp.MustCompile(`\s+`) ) // Positions of substrings in regular expressions. const ( colorForegroundPos = 1 colorBackgroundPos = 3 colorFlagPos = 5 ) // Predefined InputField acceptance functions. var ( // InputFieldInteger accepts integers. InputFieldInteger func(text string, ch rune) bool // InputFieldFloat accepts floating-point numbers. InputFieldFloat func(text string, ch rune) bool // InputFieldMaxLength returns an input field accept handler which accepts // input strings up to a given length. Use it like this: // // inputField.SetAcceptanceFunc(InputFieldMaxLength(10)) // Accept up to 10 characters. InputFieldMaxLength func(maxLength int) func(text string, ch rune) bool ) // Package initialization. func init() { // Initialize the predefined input field handlers. InputFieldInteger = func(text string, ch rune) bool { if text == "-" { return true } _, err := strconv.Atoi(text) return err == nil } InputFieldFloat = func(text string, ch rune) bool { if text == "-" || text == "." || text == "-." { return true } _, err := strconv.ParseFloat(text, 64) return err == nil } InputFieldMaxLength = func(maxLength int) func(text string, ch rune) bool { return func(text string, ch rune) bool { return len([]rune(text)) <= maxLength } } } // styleFromTag takes the given style, defined by a foreground color (fgColor), // a background color (bgColor), and style attributes, and modifies it based on // the substrings (tagSubstrings) extracted by the regular expression for color // tags. The new colors and attributes are returned where empty strings mean // "don't modify" and a dash ("-") means "reset to default". func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings []string) (newFgColor, newBgColor, newAttributes string) { if tagSubstrings[colorForegroundPos] != "" { color := tagSubstrings[colorForegroundPos] if color == "-" { fgColor = "-" } else if color != "" { fgColor = color } } if tagSubstrings[colorBackgroundPos-1] != "" { color := tagSubstrings[colorBackgroundPos] if color == "-" { bgColor = "-" } else if color != "" { bgColor = color } } if tagSubstrings[colorFlagPos-1] != "" { flags := tagSubstrings[colorFlagPos] if flags == "-" { attributes = "-" } else if flags != "" { attributes = flags } } return fgColor, bgColor, attributes } // overlayStyle calculates a new style based on "style" and applying tag-based // colors/attributes to it (see also styleFromTag()). func overlayStyle(style tcell.Style, fgColor, bgColor, attributes string) tcell.Style { _, _, defAttr := style.Decompose() if fgColor != "" && fgColor != "-" { style = style.Foreground(tcell.GetColor(fgColor)) } if bgColor != "" && bgColor != "-" { style = style.Background(tcell.GetColor(bgColor)) } if attributes == "-" { style = style.Bold(defAttr&tcell.AttrBold > 0). Italic(defAttr&tcell.AttrItalic > 0). Blink(defAttr&tcell.AttrBlink > 0). Reverse(defAttr&tcell.AttrReverse > 0). Underline(defAttr&tcell.AttrUnderline > 0). Dim(defAttr&tcell.AttrDim > 0) } else if attributes != "" { style = style.Normal() for _, flag := range attributes { switch flag { case 'l': style = style.Blink(true) case 'b': style = style.Bold(true) case 'i': style = style.Italic(true) case 'd': style = style.Dim(true) case 'r': style = style.Reverse(true) case 'u': style = style.Underline(true) case 's': style = style.StrikeThrough(true) } } } return style } // decomposeString returns information about a string which may contain color // tags or region tags, depending on which ones are requested to be found. It // returns the indices of the color tags (as returned by // re.FindAllStringIndex()), the color tags themselves (as returned by // re.FindAllStringSubmatch()), the indices of region tags and the region tags // themselves, the indices of an escaped tags (only if at least color tags or // region tags are requested), the string stripped by any tags and escaped, and // the screen width of the stripped string. func decomposeString(text string, findColors, findRegions bool) (colorIndices [][]int, colors [][]string, regionIndices [][]int, regions [][]string, escapeIndices [][]int, stripped string, width int) { // Shortcut for the trivial case. if !findColors && !findRegions { return nil, nil, nil, nil, nil, text, stringWidth(text) } // Get positions of any tags. if findColors { colorIndices = colorPattern.FindAllStringIndex(text, -1) colors = colorPattern.FindAllStringSubmatch(text, -1) } if findRegions { regionIndices = regionPattern.FindAllStringIndex(text, -1) regions = regionPattern.FindAllStringSubmatch(text, -1) } escapeIndices = escapePattern.FindAllStringIndex(text, -1) // Because the color pattern detects empty tags, we need to filter them out. for i := len(colorIndices) - 1; i >= 0; i-- { if colorIndices[i][1]-colorIndices[i][0] == 2 { colorIndices = append(colorIndices[:i], colorIndices[i+1:]...) colors = append(colors[:i], colors[i+1:]...) } } // Make a (sorted) list of all tags. allIndices := make([][3]int, 0, len(colorIndices)+len(regionIndices)+len(escapeIndices)) for indexType, index := range [][][]int{colorIndices, regionIndices, escapeIndices} { for _, tag := range index { allIndices = append(allIndices, [3]int{tag[0], tag[1], indexType}) } } sort.Slice(allIndices, func(i int, j int) bool { return allIndices[i][0] < allIndices[j][0] }) // Remove the tags from the original string. var from int buf := make([]byte, 0, len(text)) for _, indices := range allIndices { if indices[2] == 2 { // Escape sequences are not simply removed. buf = append(buf, []byte(text[from:indices[1]-2])...) buf = append(buf, ']') from = indices[1] } else { buf = append(buf, []byte(text[from:indices[0]])...) from = indices[1] } } buf = append(buf, text[from:]...) stripped = string(buf) // Get the width of the stripped string. width = stringWidth(stripped) return } // Print prints text onto the screen into the given box at (x,y,maxWidth,1), // not exceeding that box. "align" is one of AlignLeft, AlignCenter, or // AlignRight. The screen's background color will not be changed. // // You can change the colors and text styles mid-text by inserting a color tag. // See the package description for details. // // Returns the number of actual bytes of the text printed (including color tags) // and the actual width used for the printed runes. func Print(screen Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) { bytes, width, _, _ := printWithStyle(screen, text, x, y, 0, maxWidth, align, tcell.StyleDefault.Foreground(color), true) return bytes, width } func PrintWithStyle(screen Screen, text string, x, y, maxWidth, align int, style tcell.Style) (int, int) { bytes, width, _, _ := printWithStyle(screen, text, x, y, 0, maxWidth, align, style, true) return bytes, width } // printWithStyle works like Print() but it takes a style instead of just a // foreground color. The skipWidth parameter specifies the number of cells // skipped at the beginning of the text. It also returns the start and end index // (exclusively) of the text actually printed. If maintainBackground is "true", // The existing screen background is not changed (i.e. the style's background // color is ignored). func printWithStyle(screen Screen, text string, x, y, skipWidth, maxWidth, align int, style tcell.Style, maintainBackground bool) (int, int, int, int) { totalWidth, totalHeight := screen.Size() if maxWidth <= 0 || len(text) == 0 || y < 0 || y >= totalHeight { return 0, 0, 0, 0 } // Decompose the text. colorIndices, colors, _, _, escapeIndices, strippedText, strippedWidth := decomposeString(text, true, false) // We want to reduce all alignments to AlignLeft. if align == AlignRight { if strippedWidth-skipWidth <= maxWidth { // There's enough space for the entire text. return printWithStyle(screen, text, x+maxWidth-strippedWidth+skipWidth, y, skipWidth, maxWidth, AlignLeft, style, maintainBackground) } // Trim characters off the beginning. var ( bytes, width, colorPos, escapePos, tagOffset, from, to int foregroundColor, backgroundColor, attributes string ) originalStyle := style iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { // Update color/escape tag offset and style. if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] { foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) style = overlayStyle(originalStyle, foregroundColor, backgroundColor, attributes) tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0] colorPos++ } if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] { tagOffset++ escapePos++ } if strippedWidth-screenPos <= maxWidth { // We chopped off enough. if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] { // Unescape open escape sequences. escapeCharPos := escapeIndices[escapePos-1][1] - 2 text = text[:escapeCharPos] + text[escapeCharPos+1:] } // Print and return. bytes, width, from, to = printWithStyle(screen, text[textPos+tagOffset:], x, y, 0, maxWidth, AlignLeft, style, maintainBackground) from += textPos + tagOffset to += textPos + tagOffset return true } return false }) return bytes, width, from, to } else if align == AlignCenter { if strippedWidth-skipWidth == maxWidth { // Use the exact space. return printWithStyle(screen, text, x, y, skipWidth, maxWidth, AlignLeft, style, maintainBackground) } else if strippedWidth-skipWidth < maxWidth { // We have more space than we need. half := (maxWidth - strippedWidth + skipWidth) / 2 return printWithStyle(screen, text, x+half, y, skipWidth, maxWidth-half, AlignLeft, style, maintainBackground) } else { // Chop off runes until we have a perfect fit. var choppedLeft, choppedRight, leftIndex, rightIndex int rightIndex = len(strippedText) for rightIndex-1 > leftIndex && strippedWidth-skipWidth-choppedLeft-choppedRight > maxWidth { if skipWidth > 0 || choppedLeft < choppedRight { // Iterate on the left by one character. iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { if skipWidth > 0 { skipWidth -= screenWidth strippedWidth -= screenWidth } else { choppedLeft += screenWidth } leftIndex += textWidth return true }) } else { // Iterate on the right by one character. iterateStringReverse(strippedText[leftIndex:rightIndex], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { choppedRight += screenWidth rightIndex -= textWidth return true }) } } // Add tag offsets and determine start style. var ( colorPos, escapePos, tagOffset int foregroundColor, backgroundColor, attributes string ) originalStyle := style for index := range strippedText { // We only need the offset of the left index. if index > leftIndex { // We're done. if escapePos > 0 && leftIndex+tagOffset-1 >= escapeIndices[escapePos-1][0] && leftIndex+tagOffset-1 < escapeIndices[escapePos-1][1] { // Unescape open escape sequences. escapeCharPos := escapeIndices[escapePos-1][1] - 2 text = text[:escapeCharPos] + text[escapeCharPos+1:] } break } // Update color/escape tag offset. if colorPos < len(colorIndices) && index+tagOffset >= colorIndices[colorPos][0] && index+tagOffset < colorIndices[colorPos][1] { if index <= leftIndex { foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) style = overlayStyle(originalStyle, foregroundColor, backgroundColor, attributes) } tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0] colorPos++ } if escapePos < len(escapeIndices) && index+tagOffset >= escapeIndices[escapePos][0] && index+tagOffset < escapeIndices[escapePos][1] { tagOffset++ escapePos++ } } bytes, width, from, to := printWithStyle(screen, text[leftIndex+tagOffset:], x, y, 0, maxWidth, AlignLeft, style, maintainBackground) from += leftIndex + tagOffset to += leftIndex + tagOffset return bytes, width, from, to } } // Draw text. var ( drawn, drawnWidth, colorPos, escapePos, tagOffset, from, to int foregroundColor, backgroundColor, attributes string ) iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool { // Skip character if necessary. if skipWidth > 0 { skipWidth -= screenWidth from = textPos + length to = from return false } // Only continue if there is still space. if drawnWidth+screenWidth > maxWidth || x+drawnWidth >= totalWidth { return true } // Handle color tags. for colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] { foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos]) tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0] colorPos++ } // Handle escape tags. if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] { if textPos+tagOffset == escapeIndices[escapePos][1]-2 { tagOffset++ escapePos++ } } // Memorize positions. to = textPos + length // Print the rune sequence. finalX := x + drawnWidth finalStyle := style if maintainBackground { _, _, existingStyle, _ := screen.GetContent(finalX, y) _, background, _ := existingStyle.Decompose() finalStyle = finalStyle.Background(background) } finalStyle = overlayStyle(finalStyle, foregroundColor, backgroundColor, attributes) for offset := screenWidth - 1; offset >= 0; offset-- { // To avoid undesired effects, we populate all cells. if offset == 0 { screen.SetContent(finalX+offset, y, main, comb, finalStyle) } else { screen.SetContent(finalX+offset, y, ' ', nil, finalStyle) } } // Advance. drawn += length drawnWidth += screenWidth return false }) return drawn + tagOffset + len(escapeIndices), drawnWidth, from, to } // PrintSimple prints white text to the screen at the given position. func PrintSimple(screen Screen, text string, x, y int) { Print(screen, text, x, y, math.MaxInt32, AlignLeft, Styles.PrimaryTextColor) } // TaggedStringWidth returns the width of the given string needed to print it on // screen. The text may contain color tags which are not counted. func TaggedStringWidth(text string) int { _, _, _, _, _, _, width := decomposeString(text, true, false) return width } // stringWidth returns the number of horizontal cells needed to print the given // text. It splits the text into its grapheme clusters, calculates each // cluster's width, and adds them up to a total. func stringWidth(text string) (width int) { g := uniseg.NewGraphemes(text) for g.Next() { var chWidth int for _, r := range g.Runes() { chWidth = runewidth.RuneWidth(r) if chWidth > 0 { break // Our best guess at this point is to use the width of the first non-zero-width rune. } } width += chWidth } return } // WordWrap splits a text such that each resulting line does not exceed the // given screen width. Possible split points are after any punctuation or // whitespace. Whitespace after split points will be dropped. // // This function considers color tags to have no width. // // Text is always split at newline characters ('\n'). func WordWrap(text string, width int) (lines []string) { colorTagIndices, _, _, _, escapeIndices, strippedText, _ := decomposeString(text, true, false) // Find candidate breakpoints. breakpoints := boundaryPattern.FindAllStringSubmatchIndex(strippedText, -1) // Results in one entry for each candidate. Each entry is an array a of // indices into strippedText where a[6] < 0 for newline/punctuation matches // and a[4] < 0 for whitespace matches. // Process stripped text one character at a time. var ( colorPos, escapePos, breakpointPos, tagOffset int lastBreakpoint, lastContinuation, currentLineStart int lineWidth, overflow int forceBreak bool ) unescape := func(substr string, startIndex int) string { // A helper function to unescape escaped tags. for index := escapePos; index >= 0; index-- { if index < len(escapeIndices) && startIndex > escapeIndices[index][0] && startIndex < escapeIndices[index][1]-1 { pos := escapeIndices[index][1] - 2 - startIndex return substr[:pos] + substr[pos+1:] } } return substr } iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool { // Handle tags. for { if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] { // Colour tags. tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0] colorPos++ } else if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 { // Escape tags. tagOffset++ escapePos++ } else { break } } // Is this a breakpoint? if breakpointPos < len(breakpoints) && textPos+tagOffset == breakpoints[breakpointPos][0] { // Yes, it is. Set up breakpoint infos depending on its type. lastBreakpoint = breakpoints[breakpointPos][0] + tagOffset lastContinuation = breakpoints[breakpointPos][1] + tagOffset overflow = 0 forceBreak = main == '\n' if breakpoints[breakpointPos][6] < 0 && !forceBreak { lastBreakpoint++ // Don't skip punctuation. } breakpointPos++ } // Check if a break is warranted. if forceBreak || lineWidth > 0 && lineWidth+screenWidth > width { breakpoint := lastBreakpoint continuation := lastContinuation if forceBreak { breakpoint = textPos + tagOffset continuation = textPos + tagOffset + 1 lastBreakpoint = 0 overflow = 0 } else if lastBreakpoint <= currentLineStart { breakpoint = textPos + tagOffset continuation = textPos + tagOffset overflow = 0 } lines = append(lines, unescape(text[currentLineStart:breakpoint], currentLineStart)) currentLineStart, lineWidth, forceBreak = continuation, overflow, false } // Remember the characters since the last breakpoint. if lastBreakpoint > 0 && lastContinuation <= textPos+tagOffset { overflow += screenWidth } // Advance. lineWidth += screenWidth // But if we're still inside a breakpoint, skip next character (whitespace). if textPos+tagOffset < currentLineStart { lineWidth -= screenWidth } return false }) // Flush the rest. if currentLineStart < len(text) { lines = append(lines, unescape(text[currentLineStart:], currentLineStart)) } return } // Escape escapes the given text such that color and/or region tags are not // recognized and substituted by the print functions of this package. For // example, to include a tag-like string in a box title or in a TextView: // // box.SetTitle(tview.Escape("[squarebrackets]")) // fmt.Fprint(textView, tview.Escape(`["quoted"]`)) func Escape(text string) string { return nonEscapePattern.ReplaceAllString(text, "$1[]") } // iterateString iterates through the given string one printed character at a // time. For each such character, the callback function is called with the // Unicode code points of the character (the first rune and any combining runes // which may be nil if there aren't any), the starting position (in bytes) // within the original string, its length in bytes, the screen position of the // character, and the screen width of it. The iteration stops if the callback // returns true. This function returns true if the iteration was stopped before // the last character. func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool { var screenPos int gr := uniseg.NewGraphemes(text) for gr.Next() { r := gr.Runes() from, to := gr.Positions() width := stringWidth(gr.Str()) var comb []rune if len(r) > 1 { comb = r[1:] } if callback(r[0], comb, from, to-from, screenPos, width) { return true } screenPos += width } return false } // iterateStringReverse iterates through the given string in reverse, starting // from the end of the string, one printed character at a time. For each such // character, the callback function is called with the Unicode code points of // the character (the first rune and any combining runes which may be nil if // there aren't any), the starting position (in bytes) within the original // string, its length in bytes, the screen position of the character, and the // screen width of it. The iteration stops if the callback returns true. This // function returns true if the iteration was stopped before the last character. func iterateStringReverse(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool { type cluster struct { main rune comb []rune textPos, textWidth, screenPos, screenWidth int } // Create the grapheme clusters. var clusters []cluster iterateString(text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool { clusters = append(clusters, cluster{ main: main, comb: comb, textPos: textPos, textWidth: textWidth, screenPos: screenPos, screenWidth: screenWidth, }) return false }) // Iterate in reverse. for index := len(clusters) - 1; index >= 0; index-- { if callback( clusters[index].main, clusters[index].comb, clusters[index].textPos, clusters[index].textWidth, clusters[index].screenPos, clusters[index].screenWidth, ) { return true } } return false } // stripTags strips colour tags from the given string. (Region tags are not // stripped.) func stripTags(text string) string { stripped := colorPattern.ReplaceAllStringFunc(text, func(match string) string { if len(match) > 2 { return "" } return match }) return escapePattern.ReplaceAllString(stripped, `[$1$2]`) }