Skip to content

Commit

Permalink
Implement selecting from multiple cuts
Browse files Browse the repository at this point in the history
  • Loading branch information
xanni committed Oct 4, 2024
1 parent ad7b724 commit e785e83
Show file tree
Hide file tree
Showing 10 changed files with 227 additions and 22 deletions.
79 changes: 79 additions & 0 deletions edits/cuts.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package edits

import (
"strings"
"time"

ps "github.com/xanni/jotty/permascroll"
)

const (
layout = time.DateTime
minCut = 5
)

// Select previous cut.
func PrevCut() {
if ps.Cuts() > 0 {
Mode = Cuts
currentCut--
if currentCut < 1 {
currentCut = ps.Cuts()
}
}
}

// Select next cut.
func NextCut() {
if ps.Cuts() > 0 {
Mode = Cuts
currentCut++
if currentCut > ps.Cuts() {
currentCut = 1
}
}
}

func drawCut(current bool, text string, ts time.Time) (s string) {
maxLen := ex - len(layout) - 2
if maxLen < minCut {
maxLen = ex - 1
} else {
switch {
case ts.IsZero():
s = strings.Repeat(" ", len(layout)+1)
case current:
s = cutCurStyle(ts.Format(layout)) + " "
default:
s = cutTimeStyle(ts.Format(layout)) + " "
}
}

if current {
s += truncate(maxLen, text)
} else {
s += cutStyle(truncate(maxLen, text))
}

return s
}

// The preceding, current and following cuts.
func cutsWindow() (w []string) {
w = []string{cutWinStyle(strings.Repeat("—", ex))}

if currentCut > 1 {
text, ts := ps.GetCut(currentCut - 1)
w = append(w, drawCut(false, text, ts))
}

text, ts := ps.GetCut(currentCut)
w = append(w, drawCut(true, text, ts))

if currentCut < ps.Cuts() {
text, ts := ps.GetCut(currentCut + 1)
w = append(w, drawCut(false, text, ts))
}

return w
}
76 changes: 76 additions & 0 deletions edits/cuts_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package edits

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
ps "github.com/xanni/jotty/permascroll"
)

func TestDrawCut(t *testing.T) {
assert := assert.New(t)
ResizeScreen(4, 2)
assert.Equal("T…t", drawCut(false, "Test", time.Time{}))

ResizeScreen(5, 2)
assert.Equal("Test", drawCut(false, "Test", time.Time{}))

ResizeScreen(26, 2)
assert.Equal(" Test", drawCut(false, "Test", time.Time{}))
ts := time.Date(2020, time.January, 2, 3, 4, 5, 6, time.UTC)
expect := "2020-01-02 03:04:05 Test"
assert.Equal(expect, drawCut(false, "Test", ts), "unselected")
assert.Equal(expect, drawCut(true, "Test", ts), "selected")
}

func TestCutsWindow(t *testing.T) {
assert := assert.New(t)
setupTest()
ResizeScreen(5, 2)
ps.Init("I1,0:Test\nC1,0+4\n")
currentCut = 1
assert.Equal([]string{"—————", "Test"}, cutsWindow())

currentCut = ps.CopyText(1, 0, 1)
ps.CopyText(1, 2, 4)
assert.Equal([]string{"—————", "Test", "T", "st"}, cutsWindow())
}

func TestPrevCut(t *testing.T) {
assert := assert.New(t)
setupTest()

PrevCut()
assert.Equal(None, Mode)
assert.Equal(0, currentCut)

ps.Init("I1,0:Test\nC1,0+4\n")
PrevCut()
assert.Equal(Cuts, Mode)
assert.Equal(1, currentCut)

currentCut = ps.CopyText(1, 1, 3)
PrevCut()
assert.Equal(Cuts, Mode)
assert.Equal(1, currentCut)
}

func TestNextCut(t *testing.T) {
assert := assert.New(t)
setupTest()

NextCut()
assert.Equal(None, Mode)
assert.Equal(0, currentCut)

ps.Init("I1,0:Test\nC1,0+4\n")
NextCut()
assert.Equal(Cuts, Mode)
assert.Equal(1, currentCut)

currentCut = ps.CopyText(1, 1, 3)
NextCut()
assert.Equal(Cuts, Mode)
assert.Equal(1, currentCut)
}
10 changes: 7 additions & 3 deletions edits/edits.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type ModeType int

const (
None ModeType = iota
Cuts
Error
Help
Quit
Expand Down Expand Up @@ -603,7 +604,7 @@ func truncate(maxLen int, s string) string {
return s
}

half := maxLen / 2
half := (maxLen - 1) / 2

return s[:half] + string(moreChar) + s[len(s)-half:]
}
Expand Down Expand Up @@ -642,15 +643,18 @@ func Screen() string {
}

switch Mode {
case Quit:
t = append(t, confirmStyle(message))
case Cuts:
window := cutsWindow()
t = append(t[:len(t)-len(window)+1], window...)
case Error:
t = append(t, errorString()+" "+errorStyle(truncate(ex-(i18n.TextWidth["error"]+1), message)))
case Help:
window := helpWindow()
t = slices.Delete(t, 0, len(window))
t = slices.Insert(t, 0, window...)
t = append(t, statusLine())
case Quit:
t = append(t, confirmStyle(message))
default:
t = append(t, statusLine())
}
Expand Down
4 changes: 4 additions & 0 deletions edits/edits_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,10 @@ func TestScreen(t *testing.T) {
cursor[Para] = 3
assert.Equal("1 2 \n3 4\n\n_\n@14/14", Screen())

currentCut = ps.CopyText(1, 2, 7)
Mode = Cuts
assert.Equal("1 2 \n3 4\n\n——————————\nB C D", Screen())

i18n.HelpText, i18n.HelpWidth = []string{"Test"}, 4
Mode = Help
assert.Equal(" Test\n——————————\n\n_\n@14/14", Screen())
Expand Down
15 changes: 15 additions & 0 deletions edits/styles.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,21 @@ func cutStyle(s string) string {
return output.String(s).CrossOut().Foreground(output.Color(cutColor)).String()
}

// Currently selected cut.
func cutCurStyle(s string) string {
return output.String(s).Reverse().String()
}

// Timestamp of unselected cut.
func cutTimeStyle(s string) string {
return output.String(s).Reverse().Foreground(output.Color(cutColor)).String()
}

// Cut window.
func cutWinStyle(s string) string {
return output.String(s).Foreground(output.Color(cutColor)).String()
}

func errorStyle(s string) string {
return output.String(s).Foreground(output.Color(errorColor)).String()
}
Expand Down
5 changes: 3 additions & 2 deletions i18n/help.de
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ Bei ausgewähltem Text tauscht die Leertaste die Auswahlen aus,
Mit den folgenden Tasten lassen sich spezielle Aktionen ausführen:
"↑" Bereich vergrößern, "↓" Bereich verkleinern, "←" nach links bewegen, "→" nach rechts bewegen,
"Rücktaste"/"Strg-H" vorhergehenden Text löschen, "Eingabetaste"/"Strg-M" neuer Absatz,
"Tab"/"Strg-I" Markierung setzen, "Umschalt-Tab" alle Markierungen abbrechen,
"Strg-C" Text kopieren, "Strg-J" benachbarte Sätze oder Absätze verbinden,
"Einfügen"/"Strg-V" ausgeschnittenen oder kopierten Text einfügen, "Entf"/"Strg-X" Text ausschneiden,
"Pos1"/"Strg-U" zum Anfang bewegen, "Ende"/"Strg-D" zum Ende bewegen,
"Tab"/"Strg-I" Markierung setzen, "Umschalt-Tab" alle Markierungen abbrechen,
"Strg-Q"/"Strg-W" beenden, "Strg-E" exportieren, "Strg-Z" rückgängig machen, "Strg-Y" wiederherstellen
"Bild auf"/"Strg-P" wählt den vorherigen Schnitt, "Bild ab"/"Strg-N" wählt den nächsten Schnitt,
"Strg-Q"/"Strg-W" beenden, "Strg-E" exportieren, "Strg-Z" rückgängig machen, "Strg-Y" wiederherstellen.
5 changes: 3 additions & 2 deletions i18n/help.en
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ While text is selected "Space" will exchange the selections,
The following keys perform special actions:
"↑" increase scope, "↓" decrease scope, "←" move left, "→" move right,
"Backspace"/"Ctrl-H" erase preceding text, "Enter"/"Ctrl-M" new paragraph,
"Tab"/"Ctrl-I" set mark, "Shift-Tab" clear all marks,
"Ctrl-C" copy text, "Ctrl-J" join adjacent sentences or paragraphs,
"Insert"/"Ctrl-V" insert cut or copied text, "Delete"/"Ctrl-X" cut text,
"Home"/"Ctrl-U" move to beginning, "End"/"Ctrl-D" move to end,
"Tab"/"Ctrl-I" set mark, "Shift-Tab" clear all marks,
"Ctrl-Q"/"Ctrl-W" quit, "Ctrl-E" export, "Ctrl-Z" undo, "Ctrl-Y" redo
"PageUp"/"Ctrl-P" select previous cut, "PageDown"/"Ctrl-N" select next cut,
"Ctrl-Q"/"Ctrl-W" quit, "Ctrl-E" export, "Ctrl-Z" undo, "Ctrl-Y" redo.
5 changes: 3 additions & 2 deletions i18n/help.jp
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
以下のキーは特別な操作を実行します:
"↑" 範囲を広げる、"↓" 範囲を狭める、"←" 左に移動、"→" 右に移動、
"Backspace"/"Ctrl-H" で前のテキストを消去、"Enter"/"Ctrl-M" 新しい段落、
"Tab"/"Ctrl-I" でマークを設定、"Shift-Tab" で全てのマークをクリア、
"Ctrl-C" テキストをコピー、"Ctrl-J" 隣接する文や段落を結合、
"Insert"/"Ctrl-V" で切り取ったまたはコピーしたテキストを挿入、
"Delete"/"Ctrl-X" でテキストを切り取り、"Ctrl-E" でエクスポート、
"Home"/"Ctrl-U" で先頭に移動、"End"/"Ctrl-D" で末尾に移動、
"Tab"/"Ctrl-I" でマークを設定、"Shift-Tab" で全てのマークをクリア
"Ctrl-Q"/"Ctrl-W" で終了、"Ctrl-Z" で元に戻す、"Ctrl-Y" でやり直し
"PageUp"/"Ctrl-P" は前の切り取りを選択し、"PageDown"/"Ctrl-N" は次の切り取りを選択し
"Ctrl-Q"/"Ctrl-W" で終了、"Ctrl-Z" で元に戻す、"Ctrl-Y" でやり直し
2 changes: 1 addition & 1 deletion i18n/i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func init() {
s := strings.Split(string(b), "\n")
for _, t := range s {
k, v, _ := strings.Cut(t, "|")
v = strings.Replace(v, `\n`, "\n", -1)
v = strings.ReplaceAll(v, `\n`, "\n")
Text[k] = v
TextWidth[k] = uniseg.StringWidth(v)
}
Expand Down
48 changes: 36 additions & 12 deletions jotty.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import (
"strings"
"time"

tea "github.com/charmbracelet/bubbletea"
"github.com/xanni/jotty/edits"
"github.com/xanni/jotty/i18n"
ps "github.com/xanni/jotty/permascroll"
tea "github.com/charmbracelet/bubbletea"
)

//go:generate sh -c "printf %s $(git describe --always --tags) > version.txt"
Expand All @@ -36,6 +36,8 @@ var dispatch = map[tea.KeyType]func(){
tea.KeyTab: edits.Mark, tea.KeyShiftTab: edits.ClearMarks,
tea.KeyCtrlJ: edits.Join,
tea.KeyEnter: edits.Enter, tea.KeySpace: edits.Space,
tea.KeyPgDown: edits.NextCut, tea.KeyCtrlN: edits.NextCut,
tea.KeyPgUp: edits.PrevCut, tea.KeyCtrlP: edits.PrevCut,
tea.KeyCtrlQ: confirmExit, tea.KeyCtrlW: confirmExit,
tea.KeyHome: edits.Home, tea.KeyCtrlU: edits.Home,
tea.KeyInsert: edits.InsertCut, tea.KeyCtrlV: edits.InsertCut,
Expand All @@ -55,23 +57,39 @@ func export() { edits.Export(exportPath) }
func help() { edits.SetMode(edits.Help, "") }

// True if the window is sufficiently large.
func isSizeOK() bool {
return sx > 5 && sy > 2
}
func isSizeOK() bool { return sx > 5 && sy > 2 }

func (m model) Init() tea.Cmd {
edits.ID = "Jotty " + version

return nil
}

func acceptKey(m *model, msg tea.KeyMsg) {
if isSizeOK() {
m.timer.Reset(syncDelay)
if f, ok := dispatch[msg.Type]; ok {
f()
} else if msg.Type == tea.KeyRunes && !msg.Alt {
edits.InsertRunes(msg.Runes)
func (m model) acceptKey(msg tea.KeyMsg) {
m.timer.Reset(syncDelay)
if f, ok := dispatch[msg.Type]; ok {
f()
} else if msg.Type == tea.KeyRunes && !msg.Alt {
edits.InsertRunes(msg.Runes)
}
}

func (m model) cutsKey(key tea.KeyMsg) {
switch key.Type {
case tea.KeyEsc:
edits.ClearMode()
case tea.KeyPgDown, tea.KeyCtrlN:
edits.NextCut()
case tea.KeyPgUp, tea.KeyCtrlP:
edits.PrevCut()
case tea.KeySpace, tea.KeyEnter, tea.KeyInsert, tea.KeyCtrlV:
edits.ClearMode()
edits.InsertCut()
case tea.KeyRunes:
if !key.Alt {
m.timer.Reset(syncDelay)
edits.ClearMode()
edits.InsertRunes(key.Runes)
}
}
}
Expand All @@ -82,7 +100,13 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
sx, sy = msg.Width, msg.Height
edits.ResizeScreen(msg.Width, msg.Height)
case tea.KeyMsg:
if !isSizeOK() {
break
}

switch edits.Mode {
case edits.Cuts:
m.cutsKey(msg)
case edits.Error:
if msg.Type == tea.KeySpace || msg.Type == tea.KeyEnter || msg.Type == tea.KeyEsc {
edits.ClearMode()
Expand All @@ -99,7 +123,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Quit
}
default:
acceptKey(&m, msg)
m.acceptKey(msg)
}
}

Expand Down

0 comments on commit e785e83

Please sign in to comment.