rpgsheet

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit c08bb1bd1a8025db5724dc14a81cf1f3e9e74e00
parent d6a3c0ed94e6f38c0d9762e2ff9af2e882e0402c
Author: Skylar Hill <stellarskylark@posteo.net>
Date:   Wed,  1 Jun 2022 22:34:44 -0500

Add basic TUI

Diffstat:
Mexamplesheet.yaml | 43++++++++++++++++++++++++-------------------
Mrpgsheet.nimble | 1+
Msrc/rpgsheet.nim | 103+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Asrc/tui | 0
Asrc/tui.nim | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/utils.nim | 28++++++++++++++++++++++++++++
Asrc/verb.nim | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
7 files changed, 380 insertions(+), 69 deletions(-)

diff --git a/examplesheet.yaml b/examplesheet.yaml @@ -1,34 +1,39 @@ +name: Cragthar +class: Paladin (Oath of Failure) +level: 3 tabs: - main: - - ability-scores - - ability-modifiers - - skills - - misc - attacks: - - weapons + - main: + - ability-scores + - ability-modifiers + - skills + - misc + - attacks: + - weapons expressions: dexterity: - exp: "16" + modifier: 16 tab: main window: ability-scores DEX: - exp: "(dexterity-10)/2" + modifier: "(dexterity-10)/2" tab: main window: ability-modifiers - prof: - exp: "2" + proficiency: + modifier: 2 tab: main window: misc sleight-of-hand: - dice: "d20" - exp: "DEX+prof" - shortsword-atk: - dice: "d20" - exp: "DEX+prof" + dice: d20 + modifier: "DEX+proficiency" + tab: main + window: skills + shortsword-attack: + dice: d20 + modifier: "DEX+proficiency" tab: attacks window: weapons - shortsword-dmg: - dice: "d6" - exp: "DEX" + shortsword-damage: + dice: d6 + modifier: "DEX" tab: attacks window: weapons diff --git a/rpgsheet.nimble b/rpgsheet.nimble @@ -12,3 +12,4 @@ bin = @["rpgsheet"] requires "nim >= 1.6.6" requires "yaml >= 0.16.0" +requires "illwill >= 0.3.0" diff --git a/src/rpgsheet.nim b/src/rpgsheet.nim @@ -1,51 +1,54 @@ -import json, yaml, re, strutils, osproc, os - -let reRawExp = re"([a-zA-Z\-]+[a-zA-Z])" -let reProcessedExp = re"^[\d*+\-/()]*$" - -proc decodeExpression(exp: string, sheet: JsonNode): string = - var working = exp - if exp.contains(reProcessedExp): - # I think that execCmdEx adds whitespace in input; - # the xargs pipe first is necessary to prevent bc - # from throwing up - let (output, code) = execCmdEx("xargs | bc", input=exp) - if code == 0: - working = output.strip - return working - - if sheet["expressions"].contains exp: - working = decodeExpression(sheet["expressions"][exp]["exp"].getStr, sheet) - return working - - for match in exp.findAll reRawExp: - working = working.replace(match, decodeExpression(match, sheet)) - return decodeExpression(working, sheet) - -proc roll(action: string, sheet: JsonNode): string = - let exp = sheet["expressions"][action] - if not exp.contains "dice": - return decodeExpression(exp["exp"].getStr, sheet) - let dice = exp["dice"].getStr - var modifier = decodeExpression(exp["exp"].getStr, sheet) - var op = - if modifier.parseInt < 0: - "" # the minus is the negative sign in the int +import + os, + parseopt, + streams, + yaml + +import verb, tui + +let usage = """ +Usage: + rpgsheet sheet.yaml action + +Options: + -v --view View action + -i --interactive Interactive mode + (don't need to supply + an action)""" + +proc loadSheet(file: string): YamlDocument = + let stream = openFileStream(file, fmRead) + result = loadDom(stream) + stream.close() + +proc main(): void = + var arg = 0 + var sheetLocation: string + var action: string + var verb: Verb = Roll + + for kind, key, val in getopt(commandLineParams()): + case kind + of cmdArgument: + if arg == 0: + sheetLocation = key + if arg == 1: + action = key + arg += 1 + of cmdShortOption, cmdLongOption: + case key + of "v", "view": verb = View + of "i", "interactive": verb = Tui else: - "+" - let rollexp = dice & op & modifier - let (output, code) = execCmdEx("rolldice", input = rollexp) - if code == 0: - # rolldice returns its input; the actual roll is the second line - result = output.splitLines[1] - -if paramCount() < 2: - quit( - """Usage: - ./rpgsheet sheet.yaml action - """ - ) - -let file = open(paramStr(1)) -let sheetNode = file.readAll.loadtoJson[0] -echo roll(paramStr(2), sheetNode) + quit(usage) + + if arg < 2 and verb != Tui: + quit(usage) + + let sheet = loadSheet(sheetLocation) + if verb == Tui: + runTui(sheet) + return + echo doVerb(action, verb, sheet) + +main() diff --git a/src/tui b/src/tui Binary files differ. diff --git a/src/tui.nim b/src/tui.nim @@ -0,0 +1,182 @@ +import + illwill, + os, + strformat, + tables, + yaml + +import utils, verb + +proc placeWindows(tb: TerminalBuffer, windows: seq[YamlNode]): Table[string, (int, int, int, int)] = + result = initTable[string, (int, int, int, int)]() + let windowWidth = + if windows.len == 1: + tb.width - 1 + else: + tb.width div windows.len + + var currentX = 1 + for win in windows: + result[win.content] = (currentX, 3, currentX + windowWidth - 1, tb.height - 1) + currentX += windowWidth + +proc exitProc() {.noconv.} = + illwillDeinit() + showCursor() + quit(0) + +proc runTui*(sheet: YamlDocument): void = + let tabs = sheet.root["tabs"] + var currentTabIndex = 0 + + illwillInit(fullscreen=true) + setControlCHook(exitProc) + hideCursor() + + var selectedWindowIndex = 0 + var selectedByWindow = initTable[string, int]() + var itemsByWindow = initTable[string, seq[string]]() + var statusText = "" + + while true: + var tb = newTerminalBuffer(terminalWidth(), terminalHeight()) + var bb = newBoxBuffer(tb.width, tb.height) + var sbb = newBoxBuffer(tb.width, tb.height) + + var currentTabName: string + var currentTabWindows: seq[YamlNode] + # There should only be one item, but because + # fields is a Table, it's necessary to use an iterator + # to get the name and content + for n, w in tabs[currentTabIndex].fields.pairs: + currentTabName = n.content + currentTabWindows = w.elems + + let windowPositions = placeWindows(tb, currentTabWindows) + + for win in currentTabWindows: + if win.content notin itemsByWindow: + itemsByWindow[win.content] = newSeqOfCap[string](100) + + var key = getKey() + case key + of Key.Q, Key.Escape: exitProc() + of Key.ShiftJ: + if currentTabIndex < tabs.elems.high: + currentTabIndex += 1 + else: + currentTabIndex = tabs.elems.low + selectedWindowIndex = 0 + of Key.ShiftK: + if currentTabIndex > tabs.elems.low: + currentTabIndex -= 1 + else: + currentTabIndex = tabs.elems.high + selectedWindowIndex = 0 + of Key.L: + if selectedWindowIndex < currentTabWindows.high: + selectedWindowIndex += 1 + else: + selectedWindowIndex = currentTabWindows.low + of Key.H: + if selectedWindowIndex > currentTabWindows.low: + selectedWindowIndex -= 1 + else: + selectedWindowIndex = currentTabWindows.high + of Key.J: + let win = currentTabWindows[selectedWindowIndex].content + if selectedByWindow[win] < itemsByWindow[win].high: + selectedByWindow[win] = selectedByWindow[win] + 1 + else: + selectedByWindow[win] = itemsByWindow[win].low + of Key.K: + let win = currentTabWindows[selectedWindowIndex].content + if selectedByWindow[win] > itemsByWindow[win].low: + selectedByWindow[win] = selectedByWindow[win] - 1 + else: + selectedByWindow[win] = itemsByWindow[win].high + of Key.Enter: + let win = currentTabWindows[selectedWindowIndex].content + let itemList = itemsByWindow[win] + let action = itemList[selectedByWindow[win]] + statusText = fmt"{fancyDisplay(action, 99)}: {doVerb(action, Roll, sheet)}" + else: discard + + # Write title + let title = fmt"""{sheet.root.getContent("name")}, {sheet.root.getContent("class")} {sheet.root.getContent("level")}""" + let titleWidth = title.len + 3 + bb.drawRect( + 1, 0, + 1 + titleWidth, 2, + doubleStyle = true + ) + tb.write(3, 1, title) + + # Write current tab + let tabX = title.len + 5 + let tabWidth = currentTabName.len + 3 + bb.drawRect( + tabX, 0, + tabX + tabWidth, 2, + doubleStyle = true + ) + tb.write(tabX + 2, 1, currentTabName) + + # Write status line + let statusX = tabX + tabWidth + 1 + bb.drawRect( + statusX, 0, + tb.width - 1, 2, + ) + tb.write(statusX + 2, 1, statusText) + + # Write windows + for win in currentTabWindows: + let (x1, y1, x2, y2) = windowPositions[win.content] + if win == currentTabWindows[selectedWindowIndex]: + sbb.drawRect(x1, y1, x2, y2) + sbb.drawHorizLine(x1, x2, y1+2, doubleStyle=true) + else: + bb.drawRect(x1, y1, x2, y2) + bb.drawHorizLine(x1, x2, y1+2, doubleStyle=true) + + let textWidth = x2 - x1 - 4 + tb.write(x1 + 2, y1+1, fancyDisplay(win.content, textWidth)) + + # Write expressions + for name, content in sheet.root["expressions"].fields.pairs: + if content.getContent("tab") != currentTabName: + continue + + let win = content.getContent("window") + if win == "": + continue + if win notin windowPositions: + continue + + if win notin selectedByWindow: + selectedByWindow[win] = 0 + + let (x1, y1, x2, _) = windowPositions[win] + let textWidth = x2 - x1 - 4 + let sel = selectedByWindow[win] + var currentItems = itemsByWindow[win] + if name.content notin currentItems: + currentItems.add(name.content) + if sel <= currentItems.high: + if name.content == currentItems[sel]: + tb.setForegroundColor(fgBlue) + tb.write( + x1 + 2, + y1 + 3 + currentItems.find(name.content), + fancyDisplay(name.content, textWidth) + ) + tb.setForegroundColor(fgNone) + itemsbyWindow[win] = currentItems + + tb.write(bb) + tb.setForegroundColor(fgBlue) + tb.write(sbb) + tb.display() + + sleep(20) diff --git a/src/utils.nim b/src/utils.nim @@ -0,0 +1,28 @@ +import tables, strutils, yaml + +proc find*[T](list: seq[T], item: T): int = + for i in 0..list.high: + if list[i] == item: return i + result = -1 + +proc fancyDisplay*(exp: string, maxLen: int): string = + result = + if maxLen == -1: + exp + else: + exp[0..min(maxLen-1, exp.len-1)] + result = result.replace("-", " ") + for word in result.split(" "): + result = result.replace(word, word.capitalizeAscii) + +proc contains*(node: YamlNode, search: string): bool = + for key in keys(node.fields): + if key.content == search: + return true + return false + +proc getContent*(node: YamlNode, search: string): string = + for key, value in pairs(node.fields): + if key.content == search: + return value.content + return "" diff --git a/src/verb.nim b/src/verb.nim @@ -0,0 +1,92 @@ +import + osproc, + re, + streams, + strformat, + strutils, + yaml + +import utils + +let reRawExp = re"([a-zA-Z\-]+[a-zA-Z])" +let reProcessedExp = re"^[\d*+\-/()]*$" +let reDice = re"^\d*d\d+$" + +type + Verb* = enum + Roll + View + Tui + +proc expressionAsString(node: YamlNode): string = + result = fmt"""Dice: {node.getContent("dice")}""" & "\n" + result = result & fmt"""Modifier: {node.getContent("modifier")}""" & "\n" + result = result & fmt"""Tab: {node.getContent("tab")}""" & "\n" + result = result & fmt"""Window: {node.getContent("window")}""" + +proc calculateModifier(exp: string, sheet: YamlNode): string = + var working = exp + if exp.contains(reProcessedExp): + # I think that execCmdEx adds whitespace in input; + # the xargs pipe first is necessary to prevent bc + # from throwing up + let (output, code) = execCmdEx("xargs | bc", input=exp) + if code == 0: + working = output.strip + return working + + if sheet["expressions"].contains exp: + let subExp = sheet["expressions"][exp] + working = calculateModifier(subExp.getContent("modifier"), sheet) + return working + + for match in exp.findAll reRawExp: + working = working.replace(match, calculateModifier(match, sheet)) + return calculateModifier(working, sheet) + +proc roll(action: string, sheet: YamlNode): string = + if action.contains(reDice): + let (output, code) = execCmdEx("rolldice -s", input = action) + if code == 0: + # rolldice returns its input; the actual roll is the second line + return " " & output.splitLines[1] + + var exp: YamlNode + try: + exp = sheet["expressions"][action] + except KeyError: + return action & " is not a dice expression or defined expression" + + var modifier = calculateModifier(exp.getContent("modifier"), sheet) + if not exp.contains "dice": + return modifier + let dice = exp.getContent("dice") + var op = + if modifier.parseInt < 0: + "" # the minus is the negative sign in the int + else: + "+" + let rollexp = dice & op & modifier + let (output, code) = execCmdEx("rolldice -s", input = rollexp) + if code == 0: + # rolldice returns its input; the actual roll is the second line + result = output.splitLines[1] + +proc view(action: string, sheet: YamlNode): string = + let node = sheet["expressions"][action] + echo action & ":" + echo expressionAsString(node) + +proc write(doc: YamlDocument, file: string) = + let s = newfileStream(file, fmWrite) + dumpDom(doc, s, options = defineOptions(style = psBlockOnly)) + s.close() + +proc doVerb*(action: string, verb: Verb, sheet: YamlDocument): string = + case verb + of Roll: + return roll(action, sheet.root) + of View: + return view(action, sheet.root) + of Tui: + raise newException(Exception, "Please report this error, this code should never get run.")