diff --git a/.gitignore b/.gitignore index c332768..35f693a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ -*.exe -.idea/* -.vscode/* -retmp/* +*.exe +.idea/* +.vscode/* +retmp/* *.pdf \ No newline at end of file diff --git a/remarkablenim b/remarkablenim new file mode 100644 index 0000000..d7a8dd0 Binary files /dev/null and b/remarkablenim differ diff --git a/remarkablenim.nimble b/remarkablenim.nimble index 23399ae..76711f5 100644 --- a/remarkablenim.nimble +++ b/remarkablenim.nimble @@ -18,6 +18,7 @@ requires "uuids" requires "stacks" requires "weave" requires "zippy" +requires "iup" # Also requires the user shell to have scp and ssh commands # (They can be obtained via OpenSSH in windows, or via the desired distribution on linux) diff --git a/src/cmd/download.nim b/src/cmd/download.nim new file mode 100644 index 0000000..d61aa8b --- /dev/null +++ b/src/cmd/download.nim @@ -0,0 +1,28 @@ +# Tool to download a file / whole folder from the remarkable to a destination +import ../tablet/filesystem +import ../document/document +import ../tablet/downloader +import std/terminal +import std/strutils + +proc download*(elem: Element, target: string, root: bool, preset: Preset) = + var safe_str = target + if target[target.len - 1] != '/': + safe_str = safe_str & "/" + if elem.el_type == ElementType.FolderElem or elem.el_type == ElementType.RootElem: + for child in elem.children: + var subdir = safe_str + if not root: + subdir = subdir & elem.name & "/" + download(child, subdir, false, preset) + else: + let path = safe_str & elem.name & ".pdf" + let safe_path = elem.path.substr(0, elem.path.rfind(".") - 1) + let doc = download_file(safe_path, preset) + if doc == nil: + stdout.styledWriteLine(fgYellow, "Unable to download " & elem.name) + else: + echo "Generating pdf at: " & path + doc.generate_pdf(path) + + diff --git a/src/cmd/interactive.nim b/src/cmd/interactive.nim new file mode 100644 index 0000000..fd47ff7 --- /dev/null +++ b/src/cmd/interactive.nim @@ -0,0 +1,91 @@ +import std/parseopt +import std/strutils +import ../tablet/filesystem +import std/terminal +import ls +import download +import ../document/preset + +proc split_strings(a: string): seq[string] = + var buffer = "" + var in_string = false + for ch in a: + if ch == '"': + in_string = not in_string + elif (ch == ' ' and not in_string): + result.add(buffer) + buffer = "" + else: + buffer = buffer & ch + result.add(buffer) + + +proc launch_interactive*() = + # Download the filesystem + var fsroot = get_filesystem_root(false) + var tree: seq[Element] + tree.add(fsroot) + var preset = def_preset() + while true: + var cur_dir = "/" + for elem in tree: + cur_dir = cur_dir & elem.name & "/" + stdout.styledWrite(fgCyan, cur_dir & " $ ") + let input = readLine(stdin).splitStrings() + var valid = false + if input.len == 1: + if input[0] == "ls": + ls(tree[tree.len - 1]) + valid = true + elif input[0] == "exit": + break + elif input[0] == "help": + echo "ls - Displays contents of current dir" + echo "cd [dir] - Moves into a directory" + echo "save [file/dir] [destination] - Saves file into destination in your PC" + echo "sync - Does sync with all files, same as done by the GUI" + echo "setpreset [preset] - Set preset to a given one" + echo "lspresets - Display all usable presets" + valid = true + elif input.len == 2: + if input[0] == "cd": + var found = false + if input[1] == ".." and tree.len > 1: + found = true + valid = true + discard tree.pop() + else: + for elem in tree[tree.len - 1].children: + if elem.name == input[1]: + if elem.el_type == ElementType.FolderElem: + tree.add(elem) + valid = true + found = true + else: + stdout.styledWriteLine(fgRed, "Cannot move into element") + if not found: + stdout.styledWriteLine(fgRed, "Could not find element") + elif input.len == 3: + if input[0] == "save": + var found = false + var telem: Element + if input[1] == ".": + found = true + valid = true + telem = tree[tree.len - 1] + else: + for elem in tree[tree.len - 1].children: + if elem.name == input[1]: + telem = elem + valid = true + found = true + if not found: + stdout.styledWriteLine(fgRed, "Could not find element") + else: + download(telem, input[2], true, preset) + + if not valid: + stdout.styledWriteLine(fgRed, "Invalid command") + + # We support ls [dir] cd [dir] save [file/dir] [to] + diff --git a/src/cmd/ls.nim b/src/cmd/ls.nim new file mode 100644 index 0000000..2b214d3 --- /dev/null +++ b/src/cmd/ls.nim @@ -0,0 +1,11 @@ +# Tool to inspect the remarkable filesystem from the command line +import ../tablet/filesystem +import terminal + +proc ls*(fs: Element) = + for val in fs.children: + if val.el_type == ElementType.FolderElem: + stdout.styledWriteLine(fgBlue, val.name & "/") + for val in fs.children: + if val.el_type == ElementType.DocumentElem: + stdout.styledWriteLine(val.name) \ No newline at end of file diff --git a/src/document/brush.nim b/src/document/brush.nim index aa79cfb..07b4728 100644 --- a/src/document/brush.nim +++ b/src/document/brush.nim @@ -1,31 +1,31 @@ - -type RemarkableColor* = enum - BLACK, - GRAY, - WHITE, - HIGHLIGHT_YELLOW, - HIGHLIGHT_PINK, - HIGHLIGHT_GREEN, - BLUE, - RED, - HIGHLIGHT_OVERLAP - -# Includes rmhacks tools -type RemarkableTool* = enum - BRUSH, - PENCIL, - BALLPOINT, - MARKER, - FINELINER, - HIGHLIGHTER, - ERASER, - MECHANICAL, - ERASER_AREA, - CALLIGRAPHY, - UNKNOWN - -# This is used as the index LUT for the .lines files -let pen_lut* = [BRUSH, PENCIL, BALLPOINT, MARKER, FINELINER, HIGHLIGHTER, ERASER, MECHANICAL, - ERASER_AREA, UNKNOWN, UNKNOWN, UNKNOWN, BRUSH, MECHANICAL, PENCIL, BALLPOINT, MARKER, - FINELINER, HIGHLIGHTER, ERASER, UNKNOWN, CALLIGRAPHY] - + +type RemarkableColor* = enum + BLACK, + GRAY, + WHITE, + HIGHLIGHT_YELLOW, + HIGHLIGHT_PINK, + HIGHLIGHT_GREEN, + BLUE, + RED, + HIGHLIGHT_OVERLAP + +# Includes rmhacks tools +type RemarkableTool* = enum + BRUSH, + PENCIL, + BALLPOINT, + MARKER, + FINELINER, + HIGHLIGHTER, + ERASER, + MECHANICAL, + ERASER_AREA, + CALLIGRAPHY, + UNKNOWN + +# This is used as the index LUT for the .lines files +let pen_lut* = [BRUSH, PENCIL, BALLPOINT, MARKER, FINELINER, HIGHLIGHTER, ERASER, MECHANICAL, + ERASER_AREA, UNKNOWN, UNKNOWN, UNKNOWN, BRUSH, MECHANICAL, PENCIL, BALLPOINT, MARKER, + FINELINER, HIGHLIGHTER, ERASER, UNKNOWN, CALLIGRAPHY] + diff --git a/src/document/brushes/fineliner.nim b/src/document/brushes/fineliner.nim index e4375ae..3cbc8f5 100644 --- a/src/document/brushes/fineliner.nim +++ b/src/document/brushes/fineliner.nim @@ -1,19 +1,19 @@ -import nimPDF/nimPDF -import ../lines - -# color is already set in stroke and fill color -proc draw_fineliner*(x: Stroke, to: var PDF, scale: float) = - to.saveState() - - # Values found by guesstimation - to.setLineWidth(scale * 0.4) - to.setLineCap(ROUND_END) - to.setLineJoin(MITER_JOIN) - to.setMiterLimit(1.0) - to.moveTo(x.segments[0].x * SCALE_FACTOR, x.segments[0].y * SCALE_FACTOR) - for i in 1..(x.segments.len - 1): - let next = x.segments[i] - to.lineTo(next.x * SCALE_FACTOR, next.y * SCALE_FACTOR) - to.stroke() - +import nimPDF/nimPDF +import ../lines + +# color is already set in stroke and fill color +proc draw_fineliner*(x: Stroke, to: var PDF, scale: float) = + to.saveState() + + # Values found by guesstimation + to.setLineWidth(scale * 0.4) + to.setLineCap(ROUND_END) + to.setLineJoin(MITER_JOIN) + to.setMiterLimit(1.0) + to.moveTo(x.segments[0].x * SCALE_FACTOR, x.segments[0].y * SCALE_FACTOR) + for i in 1..(x.segments.len - 1): + let next = x.segments[i] + to.lineTo(next.x * SCALE_FACTOR, next.y * SCALE_FACTOR) + to.stroke() + to.restoreState() \ No newline at end of file diff --git a/src/document/brushes/highlighter.nim b/src/document/brushes/highlighter.nim index aa27d9b..3f0cef0 100644 --- a/src/document/brushes/highlighter.nim +++ b/src/document/brushes/highlighter.nim @@ -1,20 +1,20 @@ -import nimPDF/nimPDF -import ../lines - -# color is already set in stroke and fill color -proc draw_highlighter*(x: Stroke, to: var PDF, scale: float) = - to.saveState() - - # TODO: Doesn't quite match remarkable results - to.setLineWidth(scale * 3.0) - to.setLineCap(SQUARE_END) - to.setLineJoin(BEVEL_JOIN) - to.setAlpha(0.5) - to.setBlendMode(BM_OVERLAY) - to.moveTo(x.segments[0].x * SCALE_FACTOR, x.segments[0].y * SCALE_FACTOR) - for i in 1..(x.segments.len - 1): - let next = x.segments[i] - to.lineTo(next.x * SCALE_FACTOR, next.y * SCALE_FACTOR) - to.stroke() - +import nimPDF/nimPDF +import ../lines + +# color is already set in stroke and fill color +proc draw_highlighter*(x: Stroke, to: var PDF, scale: float) = + to.saveState() + + # TODO: Doesn't quite match remarkable results + to.setLineWidth(scale * 3.0) + to.setLineCap(SQUARE_END) + to.setLineJoin(BEVEL_JOIN) + to.setAlpha(0.5) + to.setBlendMode(BM_OVERLAY) + to.moveTo(x.segments[0].x * SCALE_FACTOR, x.segments[0].y * SCALE_FACTOR) + for i in 1..(x.segments.len - 1): + let next = x.segments[i] + to.lineTo(next.x * SCALE_FACTOR, next.y * SCALE_FACTOR) + to.stroke() + to.restoreState() \ No newline at end of file diff --git a/src/document/document.nim b/src/document/document.nim index a3ca51d..03862f7 100644 --- a/src/document/document.nim +++ b/src/document/document.nim @@ -1,58 +1,62 @@ -import nimPDF/nimPDF except Page -import lines -import std/json -import std/streams -import preset -import std/tables -import std/options -import eminim -import uuids - -type Document* = ref object - path: string - has_base_pdf: bool - # For each page in the generated PDF, does the page go on top of the - # original PDF, or is it a note page (true)? - page_map: seq[bool] - pages: seq[Page] - preset: Preset - - -# Assumes all needed files are downloaded -proc generate*(path: string, preset: Preset): Document = - result = new(Document) - result.path = path - - result.preset = preset - - let contents = parseFile("./retmp/data/" & path & ".content") - let pages = contents["pages"] - for page in pages.items: - let strm = newFileStream("./retmp/data/" & path & "/" & page.getStr & ".rm") - var npage = new(Page) - # PDF files may not actually have the files if the page is empty, we generate - # it anyway as we will superimpose the pdf files later - if strm.isNil: - discard - else: - npage = load_page(strm).get() - strm.close() - result.pages.add(npage) - -proc generate_pdf*(x: Document, to_path: string) = - var doc = newPDF() - var tmp_path = to_path - if x.has_base_pdf: - tmp_path = "./retmp/" & x.path & "-tmp.pdf" - - for page in x.pages: - doc.addPage(PageSize(width: fromMM(SCREEN_WIDTH), height: fromMM(SCREEN_HEIGHT)), PGO_PORTRAIT) - page.draw(doc, x.preset) - - var file = newFileStream(tmp_path, fmWrite) - doc.writePDF(file) - file.close() - - if x.has_base_pdf: - # Combine the two +import nimPDF/nimPDF except Page +import lines +import std/json +import std/streams +import preset +import std/tables +import std/options +import eminim +import os +import uuids + +type Document* = ref object + path: string + has_base_pdf*: bool + # For each page in the generated PDF, does the page go on top of the + # original PDF, or is it a note page (true)? + page_map: seq[bool] + pages: seq[Page] + preset: Preset + + +# Assumes all needed files are downloaded +proc generate*(path: string, preset: Preset): Document = + result = new(Document) + result.path = path + + result.preset = preset + + let contents = parseFile("./retmp/data/" & path & ".content") + let pages = contents["pages"] + for page in pages.items: + let strm = newFileStream("./retmp/data/" & path & "/" & page.getStr & ".rm") + var npage = new(Page) + # PDF files may not actually have the files if the page is empty, we generate + # it anyway as we will superimpose the pdf files later + if strm.isNil: + discard + else: + npage = load_page(strm).get() + strm.close() + result.pages.add(npage) + +proc generate_pdf*(x: Document, to_path: string) = + var doc = newPDF() + var tmp_path = to_path + if x.has_base_pdf: + tmp_path = "./retmp/" & x.path & "-tmp.pdf" + + for page in x.pages: + doc.addPage(PageSize(width: fromMM(SCREEN_WIDTH), height: fromMM(SCREEN_HEIGHT)), PGO_PORTRAIT) + page.draw(doc, x.preset) + + # Walk the path if it doesnt' exist + for p in to_path.parentDir.parentDirs: + discard existsOrCreateDir(p) + var file = newFileStream(tmp_path, fmWrite) + doc.writePDF(file) + file.close() + + if x.has_base_pdf: + # Combine the two discard \ No newline at end of file diff --git a/src/document/lines.nim b/src/document/lines.nim index 9927e51..0fc1ca7 100644 --- a/src/document/lines.nim +++ b/src/document/lines.nim @@ -1,110 +1,111 @@ -# Understand the .lines file from remarkable and allows writing them to a .pdf -# Supported version: 5 -# We use little endian data! -# Based on RCU code - -import std/[options, streams] -import brush -export options -import nimPDF/nimPDF -import preset -import std/tables - -# in millimeters -let SCREEN_WIDTH* = (1404.0 / 226.0) * 25.4 -let SCREEN_HEIGHT* = (1872.0 / 226.0) * 25.4 -let SCALE_FACTOR* = 25.4 / 226.0 - -type Segment* = object - x*, y*, speed*, direction*, width*, pressure*: float32 - -type Stroke* = object - pen: RemarkableTool - color: RemarkableColor - width*: float32 - segments*: seq[Segment] - -type Layer* = object - strokes: seq[Stroke] - -type Page* = ref object - layers: seq[Layer] - -proc load_segment(file: FileStream): Segment = - result.x = file.readFloat32() - result.y = file.readFloat32() - result.speed = file.readFloat32() - result.direction = file.readFloat32() - result.width = file.readFloat32() - result.pressure = file.readFloat32() - - -proc load_stroke(file: FileStream): Stroke = - let pen_int = file.readUInt32() - result.pen = pen_lut[pen_int] - let color_int = file.readUInt32() - result.color = RemarkableColor(color_int) - discard file.readInt32() - result.width = file.readFloat32() - discard file.readInt32() - let nsegments = file.readUInt32() - result.segments.setLen(nsegments) - for i in 0..(nsegments - 1): - result.segments[i] = file.load_segment() - - -proc load_page*(file: FileStream): Option[Page] = - var page = new(Page) - # Check header - if file.readStr(33) != "reMarkable .lines file, version=5": - echo "Error parsing page, incorrect notebook version?" - return none(Page) - # Read more characters that are padding - discard file.readStr(10) - # Read the number of layers - let layers = file.readInt32() - page.layers.setLen(layers) - for l in 0..(layers - 1): - # Number of strokes as unsigned 32 byte int - let nstrokes = file.readUint32() - page.layers[l].strokes.setLen(nstrokes) - for s in 0..(nstrokes - 1): - page.layers[l].strokes[s] = file.load_stroke() - - return some(page) - -import brushes/fineliner -import brushes/highlighter - -proc draw*(x: Stroke, to: var PDF, preset: Preset) = - let tool_sets = preset.tool_settings[x.pen] - if tool_sets.does_export and preset.color_exports[x.color]: - var color: Color - if tool_sets.color_map.hasKey(x.color): - color = tool_sets.color_map[x.color] - else: - color = preset.color_map[x.color] - - to.setStrokeColor(color.red.float / 255.0, color.green.float / 255.0, color.blue.float / 255.0) - - case tool_sets.draw_mode: - of BRUSH: discard - of PENCIL: discard - of BALLPOINT: discard - of MARKER: discard - of FINELINER: x.draw_fineliner(to, tool_sets.draw_scale) - of HIGHLIGHTER: x.draw_highlighter(to, tool_sets.draw_scale) - of ERASER: discard - of MECHANICAL: discard - of ERASER_AREA: discard - of CALLIGRAPHY: discard - of UNKNOWN: discard - - -proc draw*(x: Page, to: var PDF, preset: Preset) = - for layer in x.layers: - for stroke in layer.strokes: - stroke.draw(to, preset) - - - +# Understand the .lines file from remarkable and allows writing them to a .pdf +# Supported version: 5 +# We use little endian data! +# Based on RCU code + +import std/[options, streams] +import brush +export options +import nimPDF/nimPDF +import preset +import std/tables + +# in millimeters +let SCREEN_WIDTH* = (1404.0 / 226.0) * 25.4 +let SCREEN_HEIGHT* = (1872.0 / 226.0) * 25.4 +let SCALE_FACTOR* = 25.4 / 226.0 + +type Segment* = object + x*, y*, speed*, direction*, width*, pressure*: float32 + +type Stroke* = object + pen: RemarkableTool + color: RemarkableColor + width*: float32 + segments*: seq[Segment] + +type Layer* = object + strokes: seq[Stroke] + +type Page* = ref object + layers: seq[Layer] + +proc load_segment(file: FileStream): Segment = + result.x = file.readFloat32() + result.y = file.readFloat32() + result.speed = file.readFloat32() + result.direction = file.readFloat32() + result.width = file.readFloat32() + result.pressure = file.readFloat32() + + +proc load_stroke(file: FileStream): Stroke = + let pen_int = file.readUInt32() + result.pen = pen_lut[pen_int] + let color_int = file.readUInt32() + result.color = RemarkableColor(color_int) + discard file.readInt32() + result.width = file.readFloat32() + discard file.readInt32() + let nsegments = file.readUInt32() + result.segments.setLen(nsegments) + for i in 0..(nsegments - 1): + result.segments[i] = file.load_segment() + + +proc load_page*(file: FileStream): Option[Page] = + var page = new(Page) + # Check header + if file.readStr(33) != "reMarkable .lines file, version=5": + echo "Error parsing page, incorrect notebook version?" + return none(Page) + # Read more characters that are padding + discard file.readStr(10) + # Read the number of layers + let layers = file.readInt32() + page.layers.setLen(layers) + for l in 0..(layers - 1): + # Number of strokes as unsigned 32 byte int + let nstrokes = file.readUint32() + if nstrokes == 0: continue + page.layers[l].strokes.setLen(nstrokes) + for s in 0..(nstrokes - 1): + page.layers[l].strokes[s] = file.load_stroke() + + return some(page) + +import brushes/fineliner +import brushes/highlighter + +proc draw*(x: Stroke, to: var PDF, preset: Preset) = + let tool_sets = preset.tool_settings[x.pen] + if tool_sets.does_export and preset.color_exports[x.color]: + var color: Color + if tool_sets.color_map.hasKey(x.color): + color = tool_sets.color_map[x.color] + else: + color = preset.color_map[x.color] + + to.setStrokeColor(color.red.float / 255.0, color.green.float / 255.0, color.blue.float / 255.0) + + case tool_sets.draw_mode: + of BRUSH: discard + of PENCIL: discard + of BALLPOINT: discard + of MARKER: discard + of FINELINER: x.draw_fineliner(to, tool_sets.draw_scale) + of HIGHLIGHTER: x.draw_highlighter(to, tool_sets.draw_scale) + of ERASER: discard + of MECHANICAL: discard + of ERASER_AREA: discard + of CALLIGRAPHY: discard + of UNKNOWN: discard + + +proc draw*(x: Page, to: var PDF, preset: Preset) = + for layer in x.layers: + for stroke in layer.strokes: + stroke.draw(to, preset) + + + diff --git a/src/document/preset.nim b/src/document/preset.nim index b924b68..6ab0799 100644 --- a/src/document/preset.nim +++ b/src/document/preset.nim @@ -1,79 +1,79 @@ -import std/tables -import uuids -import eminim -import nigui -import std/streams -import std/os -import brush - -export Color - -type ToolSettings* = ref object - does_export*: bool - draw_scale*: float - # Non present entries use the color_map - color_map*: Table[RemarkableColor, Color] - # What does this tool draw like? To use custom drawings or remap stuff - draw_mode*: RemarkableTool - -proc def_tool_settings*(tool: RemarkableTool): ToolSettings = - ToolSettings(does_export: true, draw_scale: 1.0, color_map: initTable[RemarkableColor, Color](), draw_mode: tool) - -type Preset* = ref object - uuid*: string - name*: string - icon*: string - icon_tint*: Color - color_map*: array[RemarkableColor, Color] - color_exports*: array[RemarkableColor, bool] - tool_settings*: array[RemarkableTool, ToolSettings] - use_text_ocr*: bool - - export_templates*: bool - export_note_pages*: bool - export_base_pages*: bool - -proc def_preset*(): Preset = - result = new(Preset) - result.uuid = "66d6d990-2fd8-4e31-8260-a53c41a71429" - result.name = "Default" - result.icon = "📓" - result.icon_tint = rgb(255, 255, 255, 255) - result.color_map[BLACK] = rgb(0, 0, 0, 255) - result.color_map[GRAY] = rgb(128, 128, 128, 255) - result.color_map[WHITE] = rgb(255, 255, 255, 255) - result.color_map[HIGHLIGHT_YELLOW] = rgb(255, 245, 168, 128) - result.color_map[HIGHLIGHT_PINK] = rgb(255, 168, 254, 128) - result.color_map[HIGHLIGHT_GREEN] = rgb(167, 255, 172, 255) - result.color_map[HIGHLIGHT_OVERLAP] = rgb(0, 0, 0, 60) - result.color_map[BLUE] = rgb(60, 89, 202, 255) - result.color_map[RED] = rgb(202, 32, 32, 255) - for color in RemarkableColor: - result.color_exports[color] = true - for tool in RemarkableTool: - result.tool_settings[tool] = def_tool_settings(tool) - result.use_text_ocr = false - result.export_templates = true - result.export_note_pages = true - result.export_base_pages = true - -var all_presets*: Table[UUID, Preset] - -# Loads all the presets from the ./presets directory -proc load_presets*() = - for kind, path in walkDir("./presets/", true): - if kind != pcFile: continue - # As we load relatives, path is simply the filename - let file = newFileStream("./presets/" & path, fmRead) - let preset = file.jsonTo(Preset) - let uuid = preset.uuid.parseUUID() - all_presets[uuid] = preset - file.close() - -proc storeJson*(s: Stream; m: byte) = - storeJson(s, int(m)) - -proc save_preset*(preset: Preset) = - let file = newFileStream("./presets/" & preset.uuid & ".json", fmWrite) - file.storeJson(preset) - file.close() +import std/tables +import uuids +import eminim +import nigui +import std/streams +import std/os +import brush + +export Color + +type ToolSettings* = ref object + does_export*: bool + draw_scale*: float + # Non present entries use the color_map + color_map*: Table[RemarkableColor, Color] + # What does this tool draw like? To use custom drawings or remap stuff + draw_mode*: RemarkableTool + +proc def_tool_settings*(tool: RemarkableTool): ToolSettings = + ToolSettings(does_export: true, draw_scale: 1.0, color_map: initTable[RemarkableColor, Color](), draw_mode: tool) + +type Preset* = ref object + uuid*: string + name*: string + icon*: string + icon_tint*: Color + color_map*: array[RemarkableColor, Color] + color_exports*: array[RemarkableColor, bool] + tool_settings*: array[RemarkableTool, ToolSettings] + use_text_ocr*: bool + + export_templates*: bool + export_note_pages*: bool + export_base_pages*: bool + +proc def_preset*(): Preset = + result = new(Preset) + result.uuid = "66d6d990-2fd8-4e31-8260-a53c41a71429" + result.name = "Default" + result.icon = "📓" + result.icon_tint = rgb(255, 255, 255, 255) + result.color_map[BLACK] = rgb(0, 0, 0, 255) + result.color_map[GRAY] = rgb(128, 128, 128, 255) + result.color_map[WHITE] = rgb(255, 255, 255, 255) + result.color_map[HIGHLIGHT_YELLOW] = rgb(255, 245, 168, 128) + result.color_map[HIGHLIGHT_PINK] = rgb(255, 168, 254, 128) + result.color_map[HIGHLIGHT_GREEN] = rgb(167, 255, 172, 255) + result.color_map[HIGHLIGHT_OVERLAP] = rgb(0, 0, 0, 60) + result.color_map[BLUE] = rgb(60, 89, 202, 255) + result.color_map[RED] = rgb(202, 32, 32, 255) + for color in RemarkableColor: + result.color_exports[color] = true + for tool in RemarkableTool: + result.tool_settings[tool] = def_tool_settings(tool) + result.use_text_ocr = false + result.export_templates = true + result.export_note_pages = true + result.export_base_pages = true + +var all_presets*: Table[UUID, Preset] + +# Loads all the presets from the ./presets directory +proc load_presets*() = + for kind, path in walkDir("./presets/", true): + if kind != pcFile: continue + # As we load relatives, path is simply the filename + let file = newFileStream("./presets/" & path, fmRead) + let preset = file.jsonTo(Preset) + let uuid = preset.uuid.parseUUID() + all_presets[uuid] = preset + file.close() + +proc storeJson*(s: Stream; m: byte) = + storeJson(s, int(m)) + +proc save_preset*(preset: Preset) = + let file = newFileStream("./presets/" & preset.uuid & ".json", fmWrite) + file.storeJson(preset) + file.close() diff --git a/src/gui/base.nim b/src/gui/base.nim index 2d5ba08..6b11091 100644 --- a/src/gui/base.nim +++ b/src/gui/base.nim @@ -1,86 +1,86 @@ -import nigui - - -import weave - -init(Weave) - -import ../worker/worker -import ../document/document -import std/tables - -import notebooks_view -import uuids - -let def = def_preset() -def.save_preset() - -load_presets() - -#[let x = download("fa906f70-e0e7-4492-ac6a-1b735b2f251c", all_presets[parseUUID("66d6d990-2fd8-4e31-8260-a53c41a71429")]) -let doc = sync(x) -doc.generate_pdf("output.pdf")]# - - -let y = download("d4bd814c-dc0c-4352-b3bd-e37e8b6576d1", all_presets[parseUUID("66d6d990-2fd8-4e31-8260-a53c41a71429")]) -let doc2 = sync(y) -doc2.generate_pdf("output-pdf.pdf") - - -let MARGINS* = 8 -let STATUS_SIZE* = 28 -let BUTTONS_SIZE* = 32 - - -var selected_preset*: string - - -app.init() - -var win = newWindow("Remarkable Nim") - -win.width = 800.scaleToDpi -win.height = 600.scaleToDpi - -# The window always shows the status bar and a layout container for the other stuff -var container = newLayoutContainer(Layout_Vertical) -win.add(container) - -var main_container = newLayoutContainer(Layout_Vertical) -container.add(main_container) - - -var status_container = newLayoutContainer(Layout_Vertical) -container.add(status_container) -status_container.yAlign = YAlign_Bottom -status_container.xAlign = XAlign_Center -status_container.spacing = -18 - -var status_label = newLabel("Standby") -status_container.add(status_label) -# Make sure the progress bar is visible behind the text -status_label.backgroundColor = rgb(0, 0, 0, 0) - - -var progbar = newProgressBar() -status_container.add(progbar) - -# Dummy container to allow deletion without order change -var dummy_container = newLayoutContainer(Layout_Vertical) -main_container.add(dummy_container) - -proc goto_view(view: string, meta: string = "") = - main_container.remove(dummy_container) - case view: - of "notebooks": dummy_container = load_notebooks_view(win, meta) - else: echo "Invalid view" - main_container.add(dummy_container) - -goto_view("notebooks") - -win.show() - - -app.run() - +import nigui + + +import weave + +init(Weave) + +import ../worker/worker +import ../document/document +import std/tables + +import notebooks_view +import uuids + +let def = def_preset() +def.save_preset() + +load_presets() + +#[let x = download("fa906f70-e0e7-4492-ac6a-1b735b2f251c", all_presets[parseUUID("66d6d990-2fd8-4e31-8260-a53c41a71429")]) +let doc = sync(x) +doc.generate_pdf("output.pdf")]# + + +let y = download("d4bd814c-dc0c-4352-b3bd-e37e8b6576d1", all_presets[parseUUID("66d6d990-2fd8-4e31-8260-a53c41a71429")]) +let doc2 = sync(y) +doc2.generate_pdf("output-pdf.pdf") + + +let MARGINS* = 8 +let STATUS_SIZE* = 28 +let BUTTONS_SIZE* = 32 + + +var selected_preset*: string + + +app.init() + +var win = newWindow("Remarkable Nim") + +win.width = 800.scaleToDpi +win.height = 600.scaleToDpi + +# The window always shows the status bar and a layout container for the other stuff +var container = newLayoutContainer(Layout_Vertical) +win.add(container) + +var main_container = newLayoutContainer(Layout_Vertical) +container.add(main_container) + + +var status_container = newLayoutContainer(Layout_Vertical) +container.add(status_container) +status_container.yAlign = YAlign_Bottom +status_container.xAlign = XAlign_Center +status_container.spacing = -18 + +var status_label = newLabel("Standby") +status_container.add(status_label) +# Make sure the progress bar is visible behind the text +status_label.backgroundColor = rgb(0, 0, 0, 0) + + +var progbar = newProgressBar() +status_container.add(progbar) + +# Dummy container to allow deletion without order change +var dummy_container = newLayoutContainer(Layout_Vertical) +main_container.add(dummy_container) + +proc goto_view(view: string, meta: string = "") = + main_container.remove(dummy_container) + case view: + of "notebooks": dummy_container = load_notebooks_view(win, meta) + else: echo "Invalid view" + main_container.add(dummy_container) + +goto_view("notebooks") + +win.show() + + +app.run() + exit(Weave) \ No newline at end of file diff --git a/src/gui/notebooks_view.nim b/src/gui/notebooks_view.nim index 7a44d44..47d847e 100644 --- a/src/gui/notebooks_view.nim +++ b/src/gui/notebooks_view.nim @@ -1,199 +1,200 @@ -import nigui -import std/unicode -import std/encodings -import std/sets -import std/tables -import stacks -import std/times - -import ../tablet/filesystem -import ../document/preset - -var notebooks_text: string -var line_to_elem: Table[int, Element] -var open_folders: HashSet[Element] - -# double click detection -var last_click_time: float -var last_click_line = -1 - -# TODO: Choose a reasonable size -proc sorten(x: string): string = - if x.len > 80: - return x.substr(0, 80) & "..." - else: - return x - -proc generate(): void = - notebooks_text = "" - - let fsroot = get_filesystem_root(true) - # Walk the directory downwards (depth first traversal algorithm) - var S: Stack[Element] - var depths: Stack[int] - var visited: HashSet[Element] - S.push(fsroot) - depths.push(-1) - var line = 0 - - while not S.isEmpty: - let v = S.pop() - let curdepth = depths.pop() - if not visited.contains(v): - visited.incl(v) - if v.el_type != RootElem: - - var indent = "" - for i in 0..(curdepth - 1): - indent = indent & " " - - if v.el_type == FolderElem: - var filename = "📁 " & v.name - notebooks_text = notebooks_text & indent & sorten(filename) & "\p" - else: - var filename = all_presets[parseUUID(v.preset)].icon & " " & v.name - notebooks_text = notebooks_text & indent & sorten(filename) & "\p" - - line_to_elem[line] = v - line = line + 1 - - if (v.el_type == FolderElem and open_folders.contains(v)) or v.el_type == RootElem: - for child in v.children: - S.push(child) - depths.push(curdepth + 1) - - - - -proc load_notebooks_view*(win: Window, meta: string): LayoutContainer = - let basecont = newLayoutContainer(Layout_Vertical) - basecont.heightMode = HeightMode_Expand - # List of files - let cont = newLayoutContainer(Layout_Vertical) - let tree = newTextArea("") - tree.heightMode = HeightMode_Expand - tree.fontFamily = "Consolas" - tree.fontSize = 20 - tree.editable = false - - - cont.add(tree) - # Buttons and chooser - let subcont = newLayoutContainer(Layout_Horizontal) - subcont.yAlign = YAlign_Bottom - #subcont.heightMode = HeightMode_Fill - var presets: seq[string] - presets.add("Hello") - presets.add("world") - var dropdown = newComboBox(presets) - dropdown.widthMode = WidthMode_Expand - subcont.add(dropdown) - - var sync_opts = newButton("Sync Options") - #sync_opts.height = 28 - subcont.add(sync_opts) - - var multibut = newButton("Upload to") - multibut.onClick = proc(event: ClickEvent) = - var selected: Element = nil - if last_click_line < 0 or (selected = line_to_elem[last_click_line]; selected).el_type == FolderElem: - var dialog = newOpenFileDialog() - dialog.title = "File location" - dialog.multiple = true - dialog.run() - for file in dialog.files: - echo file - else: - var dialog = SaveFileDialog() - dialog.title = "Destination location" - dialog.defaultName = selected.name & ".pdf" - dialog.run() - echo dialog.file - - #multibut.height = 28 - subcont.add(multibut) - - basecont.add(cont) - basecont.add(subcont) - - tree.onClick = proc(event: ClickEvent) = - # text is in UTF-8 conveniently - var text = tree.text - var pos = tree.cursorPos - var rbyte = 0 - var lbyte = 0 - var lengths_up_to_lbyte: seq[int] - # positions are in characters and not bytes! - # find rbyte and lbyte (emojis take 2 pos for wathever reason) - while pos > 0: - if text.runeLenAt(rbyte) == 4: - pos = pos - 2 - else: - pos = pos - 1 - lengths_up_to_lbyte.add(text.runeLenAt(rbyte)) - rbyte = rbyte + text.runeLenAt(rbyte) - - lbyte = rbyte - if rbyte == text.len: - return - - # Find the line by counting \n up to the desired point - var line = 0 - for i in 0..lbyte: - if text[i] == '\n': - line = line + 1 - - if line_to_elem[line].el_type == FolderElem: - multibut.text = "Upload to" - else: - multibut.text = "Download" - - let dur = cpuTime() - last_click_time - last_click_time = cpuTime() - - # Double clicking opens folders - if line == last_click_line and dur < 0.4: - var elem = line_to_elem[line] - if elem.el_type == FolderElem: - if open_folders.contains(elem): - open_folders.excl(elem) - else: - open_folders.incl(elem) - generate() - tree.text = notebooks_text - return - - last_click_line = line - - var rpos = tree.cursorPos - while true: - if text[rbyte] == '\n': - break - rbyte = rbyte + text.runeLenAt(rbyte) - # For wathever reason emojis take 2 positions, keep this in mind! - if text.runeLenAt(rbyte) == 4: - rpos = rpos + 2 - else: - rpos = rpos + 1 - - var lpos = tree.cursorPos - while true: - if lengths_up_to_lbyte.len == 0 or text[lbyte] == '\n': - break - let length = lengths_up_to_lbyte[^1] - lbyte = lbyte - length - lengths_up_to_lbyte.delete(lengths_up_to_lbyte.len - 1) - if length == 4: - lpos = lpos - 2 - else: - lpos = lpos - 1 - tree.selectionStart = lpos - tree.selectionEnd = rpos - - generate() - # We need to delay a bit until app is actually started to try and get text - startTimer(10, proc(event: TimerEvent) = - tree.text = notebooks_text) - win.onResize = proc(event: ResizeEvent) = - tree.text = notebooks_text + import nigui +import std/unicode +import std/encodings +import std/sets +import std/tables +import stacks +import std/times + +import ../tablet/filesystem +import ../document/preset + +var notebooks_text: string +var line_to_elem: Table[int, Element] +var open_folders: HashSet[Element] + +# double click detection +var last_click_time: float +var last_click_line = -1 + +# TODO: Choose a reasonable size +proc sorten(x: string): string = + if x.len > 80: + return x.substr(0, 80) & "..." + else: + return x + +proc generate(): void = + notebooks_text = "" + + let fsroot = get_filesystem_root(true) + # Walk the directory downwards (depth first traversal algorithm) + var S: Stack[Element] + var depths: Stack[int] + var visited: HashSet[Element] + S.push(fsroot) + depths.push(-1) + var line = 0 + + while not S.isEmpty: + let v = S.pop() + let curdepth = depths.pop() + if not visited.contains(v): + visited.incl(v) + if v.el_type != RootElem: + + var indent = "" + for i in 0..(curdepth - 1): + indent = indent & " " + + if v.el_type == FolderElem: + var filename = "📁 " & v.name + notebooks_text = notebooks_text & indent & sorten(filename) & "\p" + else: + var filename = all_presets[parseUUID(v.preset)].icon & " " & v.name + notebooks_text = notebooks_text & indent & sorten(filename) & "\p" + + line_to_elem[line] = v + line = line + 1 + + if (v.el_type == FolderElem and open_folders.contains(v)) or v.el_type == RootElem: + for child in v.children: + S.push(child) + depths.push(curdepth + 1) + + + + +proc load_notebooks_view*(win: Window, meta: string): LayoutContainer = + let basecont = newLayoutContainer(Layout_Vertical) + basecont.heightMode = HeightMode_Expand + # List of files + let cont = newLayoutContainer(Layout_Vertical) + let tree = newTextArea("") + tree.heightMode = HeightMode_Expand + tree.fontFamily = "Consolas" + tree.fontSize = 20 + tree.editable = false + + + cont.add(tree) + # Buttons and chooser + let subcont = newLayoutContainer(Layout_Horizontal) + subcont.yAlign = YAlign_Bottom + #subcont.heightMode = HeightMode_Fill + var presets: seq[string] + presets.add("Hello") + presets.add("world") + var dropdown = newComboBox(presets) + dropdown.widthMode = WidthMode_Expand + subcont.add(dropdown) + + var sync_opts = newButton("Sync Options") + #sync_opts.height = 28 + subcont.add(sync_opts) + + var multibut = newButton("Upload to") + multibut.onClick = proc(event: ClickEvent) = + var selected: Element = nil + if last_click_line < 0 or (selected = line_to_elem[last_click_line]; selected).el_type == FolderElem: + var dialog = newOpenFileDialog() + dialog.title = "File location" + dialog.multiple = true + dialog.run() + for file in dialog.files: + echo file + else: + var dialog = SaveFileDialog() + dialog.title = "Destination location" + dialog.defaultName = selected.name & ".pdf" + dialog.run() + echo dialog.file + + #multibut.height = 28 + subcont.add(multibut) + + basecont.add(cont) + basecont.add(subcont) + + + tree.onClick = proc(event: ClickEvent) = + # text is in UTF-8 conveniently + var text = tree.text + var pos = tree.cursorPos + var rbyte = 0 + var lbyte = 0 + var lengths_up_to_lbyte: seq[int] + # positions are in characters and not bytes! + # find rbyte and lbyte (emojis take 2 pos for wathever reason) + while pos > 0: + if text.runeLenAt(rbyte) == 4: + pos = pos - 2 + else: + pos = pos - 1 + lengths_up_to_lbyte.add(text.runeLenAt(rbyte)) + rbyte = rbyte + text.runeLenAt(rbyte) + + lbyte = rbyte + if rbyte == text.len: + return + + # Find the line by counting \n up to the desired point + var line = 0 + for i in 0..lbyte: + if text[i] == '\n': + line = line + 1 + + if line_to_elem[line].el_type == FolderElem: + multibut.text = "Upload to" + else: + multibut.text = "Download" + + let dur = cpuTime() - last_click_time + last_click_time = cpuTime() + + # Double clicking opens folders + if line == last_click_line and dur < 0.4: + var elem = line_to_elem[line] + if elem.el_type == FolderElem: + if open_folders.contains(elem): + open_folders.excl(elem) + else: + open_folders.incl(elem) + generate() + tree.text = notebooks_text + return + + last_click_line = line + + var rpos = tree.cursorPos + while true: + if text[rbyte] == '\n': + break + rbyte = rbyte + text.runeLenAt(rbyte) + # For wathever reason emojis take 2 positions, keep this in mind! + if text.runeLenAt(rbyte) == 4: + rpos = rpos + 2 + else: + rpos = rpos + 1 + + var lpos = tree.cursorPos + while true: + if lengths_up_to_lbyte.len == 0 or text[lbyte] == '\n': + break + let length = lengths_up_to_lbyte[^1] + lbyte = lbyte - length + lengths_up_to_lbyte.delete(lengths_up_to_lbyte.len - 1) + if length == 4: + lpos = lpos - 2 + else: + lpos = lpos - 1 + tree.selectionStart = lpos + tree.selectionEnd = rpos + + generate() + # We need to delay a bit until app is actually started to try and get text + startTimer(10, proc(event: TimerEvent) = + tree.text = notebooks_text) + win.onResize = proc(event: ResizeEvent) = + tree.text = notebooks_text return basecont \ No newline at end of file diff --git a/src/pdf/pdfcombine.nim b/src/pdf/pdfcombine.nim index 3c706da..8484a89 100644 --- a/src/pdf/pdfcombine.nim +++ b/src/pdf/pdfcombine.nim @@ -1,114 +1,141 @@ -# Allows adding pages to PDF files / adding content on top of existing pages -# very barebones implementation, it would be a better idea to wrap a PDF library that -# can read files, but it works -import std/tables -import std/streams -import std/parseutils -import std/strutils -import json - -type PDFObjType = enum - ARRAY, - TABLE, - OTHER - -# We only really care about array and table objects -type PDFObj = ref object - case kind: PDFObjType - of ARRAY: - elems: seq[PDFObj] - of TABLE: - children: Table[string, PDFObj] - of OTHER: discard - - -type PDFMap = ref object - objects: seq[PDFObj] - -# Ignores comments -proc get_next_line(f: FileStream): string = - var in_comment = false - var line_start = true - var c: char - var line: string - while true: - c = f.readChar() - line.add(c) - if c == '%' and line_start == true: - in_comment = true - - if line_start == true: - line_start = false - - if c == '\n': - line_start = true - if not in_comment: - return line - in_comment = false - line = "" - -# We convert the weird syntax into JSON and parse it -proc parse_table(f: FileStream): JsonNode = - var as_json = "{" - var depth = 0 - assert f.get_next_line().startsWith("<<") - while true: - let line = f.get_next_line().unindent() - if line.startsWith(">>"): - # remove ',' from last elem - as_json.delete(as_json.len - 2, as_json.len - 2) - if depth != 0: - as_json.add("},\n") - depth = depth - 1 - continue - else: - as_json.add("}\n") - echo as_json - return parseJson(as_json) - var separator_loc = line.find(" ") - assert separator_loc >= 0 - var key = line.substr(1, separator_loc) - as_json.add("\"" & key & "\": ") - # Value is a bit more complicated as it may be many stuff, but for parsing - # we lump everything into a string EXCEPT sub dictionaries - var value = line.substr(separator_loc + 1) - value.removeSuffix({'\n', '\r', ' '}) - if value.startsWith("<<"): - depth = depth + 1 - as_json.add("{\n") - else: - as_json.add("\"" & value & "\",\n") - - - -# TODO: This could break on HUGE pdf files? -proc get_uid(first_num: int, second_num: int): uint64 = - return first_num.uint64 + second_num.uint64 * 4294967296'u64 - -# We only parse obj streams. TODO: This could fail on some very weird PDFs -proc parse_pdf*(path: string): PDFMap = - var file = newFileStream(path, fmRead) - - while true: - var line = file.get_next_line() - # Parse an object - if line.endsWith(" obj\n") or line.endsWith(" obj\r\n"): - # First line defines object ID - var first_num, second_num: int - let advance = line.parseInt(first_num) - line = line.substr(advance) - discard line.parseInt(second_num) - let object_uid = get_uid(first_num, second_num) - - # next lines should be a table - let table = file.parse_table() - - - file.close() - -# page_map indicates wether a page in over goes into a new page (true) -# or if it goes over the old page (ie an overlay) -# Afterwards we COULD linearize or optimize the PDF for performance, but for relatively -# small updates (which these are, a few lines on top of a long pdf) it should be good -proc overlap_pdf*(base: string, over: string, page_map: seq[bool]) = +# Allows adding pages to PDF files / adding content on top of existing pages +# very barebones implementation, it would be a better idea to wrap a PDF library that +# can read files, but it works +import std/tables +import std/streams +import std/parseutils +import std/strutils +import json +import zippy + +type PDFObjType = enum + ARRAY, + TABLE, + OTHER + +# We only really care about array and table objects +type PDFObj = ref object + case kind: PDFObjType + of ARRAY: + elems: seq[PDFObj] + of TABLE: + children: Table[string, PDFObj] + of OTHER: discard + + +type PDFMap = ref object + objects: seq[PDFObj] + +# Ignores comments +proc get_next_line(f: FileStream): string = + var in_comment = false + var line_start = true + var c: char + var line: string + while true: + c = f.readChar() + line.add(c) + if c == '%' and line_start == true: + in_comment = true + + if line_start == true: + line_start = false + + if c == '\n': + line_start = true + if not in_comment: + return line + in_comment = false + line = "" + +# We convert the weird syntax into JSON and parse it +proc parse_table(f: FileStream): JsonNode = + var as_json = "{" + var depth = 0 + assert f.get_next_line().startsWith("<<") + while true: + let line = f.get_next_line().unindent() + if line.startsWith(">>"): + # remove ',' from last elem + as_json.delete(as_json.len - 2, as_json.len - 2) + if depth != 0: + as_json.add("},\n") + depth = depth - 1 + continue + else: + as_json.add("}\n") + return parseJson(as_json) + var separator_loc = line.find(" ") + assert separator_loc >= 0 + var key = line.substr(1, separator_loc - 1) + as_json.add("\"" & key & "\": ") + # Value is a bit more complicated as it may be many stuff, but for parsing + # we lump everything into a string EXCEPT sub dictionaries + var value = line.substr(separator_loc + 1) + value.removeSuffix({'\n', '\r', ' '}) + if value.startsWith("<<"): + depth = depth + 1 + as_json.add("{\n") + else: + as_json.add("\"" & value & "\",\n") + + + +# TODO: This could break on HUGE pdf files? +proc get_uid(first_num: int, second_num: int): uint64 = + return first_num.uint64 + second_num.uint64 * 4294967296'u64 + +# We only parse obj streams. TODO: This could fail on some very weird PDFs +proc parse_pdf*(path: string): PDFMap = + var file = newFileStream(path, fmRead) + + while true: + var line = file.get_next_line() + # Parse an object + if line.endsWith(" obj\n") or line.endsWith(" obj\r\n"): + # First line defines object ID + var first_num, second_num: int + let advance = line.parseInt(first_num) + line = line.substr(advance) + discard line.parseInt(second_num) + let object_uid = get_uid(first_num, second_num) + + # next lines should be a table which defines the object + let table = file.parse_table() + + # We only care about objects without Type, Length(1/2/3) and with a Filter (decompression) + # (Length 2 and 3 always come with Length1, checking one is enough) + if not table.hasKey("Type") and not table.hasKey("Length1") and + table.hasKey("Filter") and table.hasKey("Length"): + # TODO: Implement other decodes (They are rare) + assert table["Filter"].getStr() == "/FlateDecode" + # Obtain length + var length: int + discard table["Length"].getStr().parseInt(length) + # Read stream + assert file.get_next_line().startsWith("stream") + # Now read length bytes + var bytes: seq[uint8] + bytes.setLen(length) + for i in 0..(length - 1): + bytes[i] = file.readUint8() + # we may have to read another byte (EOL) + if file.peekChar() == '\n': + discard file.readChar() + assert file.get_next_line().startsWith("endstream") + # Decode the stream + let decoded = bytes.uncompress() + var s: string + for b in decoded: + s.add(b.char) + echo s + + + file.close() + +# page_map indicates wether a page in over goes into a new page (true) +# or if it goes over the old page (ie an overlay) +# Afterwards we COULD linearize or optimize the PDF for performance, but for relatively +# small updates (which these are, a few lines on top of a long pdf) it should be good +proc overlap_pdf*(base: string, over: string, page_map: seq[bool]) = discard \ No newline at end of file diff --git a/src/remarkablenim.nim b/src/remarkablenim.nim index de28ac4..df05c3e 100644 --- a/src/remarkablenim.nim +++ b/src/remarkablenim.nim @@ -1,5 +1,9 @@ -#import gui/base - -import pdf/pdfcombine - -discard parse_pdf("./retmp/data/d4bd814c-dc0c-4352-b3bd-e37e8b6576d1.pdf") \ No newline at end of file +import os +import cmd/interactive + +# Call like remarkablenim interactive to enter interactive mode +# Call like remarkablenim gui to enter GUI mode (TODO) +echo paramCount() +if paramCount() == 1: + if paramStr(1) == "interactive": + launch_interactive() \ No newline at end of file diff --git a/src/tablet/downloader.nim b/src/tablet/downloader.nim new file mode 100644 index 0000000..866a973 --- /dev/null +++ b/src/tablet/downloader.nim @@ -0,0 +1,58 @@ +import std/[os, osproc, options, json] + +import ../document/document +import ../document/preset +import std/distros +import std/terminal + +export preset + +# returns true if failed, if ext = "/", then we are copying a folder +proc download(path: string, ext: string): bool = + let rm_path = "root@10.11.99.1:/home/root/.local/share/remarkable/xochitl/" & path & ext + let pc_path = "./retmp/data/" & path & ext + var process: Process + var exe_name = "/usr/bin/scp" + if detectOs(Windows): + exe_name = "scp" + if ext == "/": + process = startProcess(exe_name, "", ["-r", "-o", "ConnectTimeout=1", rm_path, pc_path]) + else: + process = startProcess(exe_name, "", ["-o", "ConnectTimeout=1", rm_path, pc_path]) + + let code = process.waitForExit + if code != 0: + return true + + return false + +# multiple scp instances can run without problem, so we spawn as many as needed +# (Note that this will always work with directories!) +# May return nil! +proc download_file*(path: string, preset: Preset): Document = + # We first download the .content to investigate other needed files + if download(path, ".content"): + stdout.styledWriteLine(fgRed, "Could not download .content") + return nil + # Investigate + let contents = parseFile("./retmp/data/" & path & ".content") + + # Base pdf file TODO + if contents["fileType"].getStr == "pdf": + return nil + if download(path, ".pdf"): + return nil + # Download all pages + if download(path, "/"): + stdout.styledWriteLine(fgRed, "Could not downloadpagees") + return nil + # We generate the document from this, this is also an "expensive" operation + let doc = generate(path, preset) + + # And the contents file + #removeFile("./retmp/data/" & path & ".contents") + # And other used files / folder + # We may now remove the folder + #removeDir("./retmp/data/" & path) + + return doc diff --git a/src/tablet/filesystem.nim b/src/tablet/filesystem.nim index c718c8c..36eabb1 100644 --- a/src/tablet/filesystem.nim +++ b/src/tablet/filesystem.nim @@ -1,128 +1,129 @@ -# Linked list of all folders and files -import std/[options, json, strutils, os, osproc, hashes, algorithm, tables, streams] -import eminim -import uuids -import ../document/document - -export uuids - -# Invalidate as needed externally (set to None) so we reload it -var preset_assignment*: Option[Table[string, string]] - -type - ElementType* = enum - RootElem - FolderElem - DocumentElem - - Element* = ref object - path*: string - name*: string - modify*: int - str_parent: string - parent*: Option[Element] - case el_type*: ElementType - of DocumentElem: - document_data: Document - preset*: string - of RootElem, FolderElem: children*: seq[Element] - -proc hash*(x: Element): Hash = - var h: Hash = 0 - h = h !& hash(x.path) - return !$h - -proc create_root(): Element = - return Element(path: "root", name: "root", parent: none(Element)) - -proc sort_children(e: var Element) = - if e.el_type == FolderElem or e.el_type == RootElem: - e.children.sort do (x, y: Element) -> int: - # We alphabetic sort, but give priority to folders - if x.el_type == FolderElem and y.el_type != FolderElem: - result = 1 - elif x.el_type != FolderElem and y.el_type == FolderElem: - result = -1 - else: - result = -cmp(x.name, y.name) - -proc create_base_from_json(path: string, j: JsonNode): Option[Element] = - if j["deleted"].bval == true: - return none(Element) - else: - if j["parent"].kind == JString and (j["parent"].str == "trash"): - return none(Element) - case j["type"].str - of "CollectionType": - return some(Element(path: path, name: j["visibleName"].str, modify: parseInt(j["lastModified"].getStr("0")), - parent: none(Element), el_type: FolderElem, children: newSeq[Element](), str_parent: j["parent"].getStr("root"))) - of "DocumentType": - if preset_assignment.isNone: - let file = newFileStream("./retmp/preset_assignment.json", fmRead) - preset_assignment = some(file.jsonTo(Table[string, string])) - file.close() - - let preset = preset_assignment.get.getOrDefault(path, "66d6d990-2fd8-4e31-8260-a53c41a71429") - - return some(Element(path: path, name: j["visibleName"].str, modify: parseInt(j["lastModified"].getStr("0")), - parent: none(Element), el_type: DocumentElem, str_parent: j["parent"].getStr("root"), preset: preset)) - -var fs_root: Element - -# All paths are prefixed with /home/root/.local/share/remarkable/xochitl/ -# Returns the root element of the file system -proc load_filesystem*(from_cached = false): Element = - # We only get the .metadata files for now, we copy all at once as they are lightweight - # Using the wildcard feature on scp, we can obtain all .metadata - - # First, remove all tmp contents - #[removeDir("./retmp/metadata"); - createDir("./retmp/metadata"); - - # We may now copy everything over, we do this synchronous because it should be fast - let command = "scp -q root@10.11.99.1:/home/root/.local/share/remarkable/xochitl/*.metadata ./retmp/metadata" - let res, code = execCmdEx(command)]# - var root = create_root() - var all_rm_elems: seq[Element] - - for kind, path in walkDir("./retmp/metadata/", true): - if kind != pcFile: continue - # As we load relatives, path is simply the filename - let file = parseJson(readFile("./retmp/metadata/" & path)) - let elem = create_base_from_json(path, file) - if elem.isSome: - all_rm_elems.add(elem.get()) - - # We now create the tree structure (once all files are loaded) - for elem in mitems(all_rm_elems): - if elem.str_parent == "root" or elem.str_parent == "": - elem.parent = some(root) - root.children.add(elem) - else: - for slem in mitems(all_rm_elems): - if slem.el_type != FolderElem: continue - if slem.path.startsWith(elem.str_parent): - elem.parent = some(slem) - slem.children.add(elem) - - root.sort_children() - for elem in mitems(all_rm_elems): - elem.sort_children() - - fs_root = root - return fs_root - -# May load the filesystem if needed -proc get_filesystem_root*(from_cached = false): Element = - if fs_root != nil: - return fs_root - else: - return load_filesystem(from_cached) - -# Will download needed files to retmp/data -proc get_document*(x: Element): Document = - return Document() - -# Removes used files in retmp/data -proc clean_document*(x: Element) = +# Linked list of all folders and files +import std/[options, json, strutils, os, osproc, hashes, algorithm, tables, streams] +import eminim +import uuids +import ../document/document +import ../tablet/downloader + +export uuids + +# Invalidate as needed externally (set to None) so we reload it +var preset_assignment*: Option[Table[string, string]] + +type + ElementType* = enum + RootElem + FolderElem + DocumentElem + + Element* = ref object + path*: string + name*: string + modify*: int + str_parent: string + parent*: Option[Element] + case el_type*: ElementType + of DocumentElem: + document_data: Document + preset*: string + of RootElem, FolderElem: children*: seq[Element] + +proc hash*(x: Element): Hash = + var h: Hash = 0 + h = h !& hash(x.path) + return !$h + +proc create_root(): Element = + return Element(path: "root", name: "root", parent: none(Element)) + +proc sort_children(e: var Element) = + if e.el_type == FolderElem or e.el_type == RootElem: + e.children.sort do (x, y: Element) -> int: + # We alphabetic sort, but give priority to folders + if x.el_type == FolderElem and y.el_type != FolderElem: + result = 1 + elif x.el_type != FolderElem and y.el_type == FolderElem: + result = -1 + else: + result = -cmp(x.name, y.name) + +proc create_base_from_json(path: string, j: JsonNode): Option[Element] = + if j["deleted"].bval == true: + return none(Element) + else: + if j["parent"].kind == JString and (j["parent"].str == "trash"): + return none(Element) + case j["type"].str + of "CollectionType": + return some(Element(path: path, name: j["visibleName"].str, modify: parseInt(j["lastModified"].getStr("0")), + parent: none(Element), el_type: FolderElem, children: newSeq[Element](), str_parent: j["parent"].getStr("root"))) + of "DocumentType": + if preset_assignment.isNone: + let file = newFileStream("./retmp/preset_assignment.json", fmRead) + preset_assignment = some(file.jsonTo(Table[string, string])) + file.close() + + let preset = preset_assignment.get.getOrDefault(path, "66d6d990-2fd8-4e31-8260-a53c41a71429") + + return some(Element(path: path, name: j["visibleName"].str, modify: parseInt(j["lastModified"].getStr("0")), + parent: none(Element), el_type: DocumentElem, str_parent: j["parent"].getStr("root"), preset: preset)) + +var fs_root: Element + +# All paths are prefixed with /home/root/.local/share/remarkable/xochitl/ +# Returns the root element of the file system +proc load_filesystem*(from_cached = false): Element = + # We only get the .metadata files for now, we copy all at once as they are lightweight + # Using the wildcard feature on scp, we can obtain all .metadata + + # First, remove all tmp contents + removeDir("./retmp/metadata"); + createDir("./retmp/metadata"); + + # We may now copy everything over, we do this synchronous because it should be fast + let command = "scp -q root@10.11.99.1:/home/root/.local/share/remarkable/xochitl/*.metadata ./retmp/metadata" + let res, code = execCmdEx(command) + var root = create_root() + var all_rm_elems: seq[Element] + + for kind, path in walkDir("./retmp/metadata/", true): + if kind != pcFile: continue + # As we load relatives, path is simply the filename + let file = parseJson(readFile("./retmp/metadata/" & path)) + let elem = create_base_from_json(path, file) + if elem.isSome: + all_rm_elems.add(elem.get()) + + # We now create the tree structure (once all files are loaded) + for elem in mitems(all_rm_elems): + if elem.str_parent == "root" or elem.str_parent == "": + elem.parent = some(root) + root.children.add(elem) + else: + for slem in mitems(all_rm_elems): + if slem.el_type != FolderElem: continue + if slem.path.startsWith(elem.str_parent): + elem.parent = some(slem) + slem.children.add(elem) + + root.sort_children() + for elem in mitems(all_rm_elems): + elem.sort_children() + + fs_root = root + return fs_root + +# May load the filesystem if needed +proc get_filesystem_root*(from_cached = false): Element = + if fs_root != nil: + return fs_root + else: + return load_filesystem(from_cached) + +# Will download needed files to retmp/data +proc get_document*(x: Element): Document = + return Document() + +# Removes used files in retmp/data +proc clean_document*(x: Element) = return \ No newline at end of file diff --git a/src/tablet/util.nim b/src/tablet/util.nim index 223524e..3b66255 100644 --- a/src/tablet/util.nim +++ b/src/tablet/util.nim @@ -1,8 +1,8 @@ -include std/osproc -include std/strutils - -proc check_connection(): bool = - # This uses a 1s timeout which is more than enough for USB communcations - let command = "ssh -o ConnectTimeout=1 -q root@10.11.99.1 echo ping" - let res, code = execCmdEx(command) +include std/osproc +include std/strutils + +proc check_connection(): bool = + # This uses a 1s timeout which is more than enough for USB communcations + let command = "ssh -o ConnectTimeout=1 -q root@10.11.99.1 echo ping" + let res, code = execCmdEx(command) return res.startsWith("ping") \ No newline at end of file diff --git a/src/worker/worker.nim b/src/worker/worker.nim index 46a1aa1..7cddd3f 100644 --- a/src/worker/worker.nim +++ b/src/worker/worker.nim @@ -1,60 +1,8 @@ -import weave -import deques -import std/[os, osproc, options, json] - -import ../document/document -import ../document/preset - -export preset - -# returns true if failed, if ext = "/", then we are copying a folder -proc download(path: string, ext: string): bool = - let rm_path = "root@10.11.99.1:/home/root/.local/share/remarkable/xochitl/" & path & ext - let pc_path = "./retmp/data/" & path & ext - var process: Process - if ext == "/": - process = startProcess("scp", "", ["-r", "-o", "ConnectTimeout=1", rm_path, pc_path]) - else: - process = startProcess("scp", "", ["-o", "ConnectTimeout=1", rm_path, pc_path]) - - let code = process.waitForExit - if code != 0: - return true - - return false - - -# multiple scp instances can run without problem, so we spawn as many as needed -# (Note that this will always work with directories!) -# May return nil! -proc download_worker(path: string, preset: Preset): Document = - # We first download the .content to investigate other needed files - #[if download(path, ".content"): - return nil - - # Investigate - let contents = parseFile("./retmp/data/" & path & ".content") - - # Base pdf file - if contents["fileType"].getStr == "pdf": - if download(path, ".pdf"): - return nil - - # Download all pages - if download(path, "/"): - return nil - ]# - # We generate the document from this, this is also an "expensive" operation - let doc = generate(path, preset) - - # And the contents file - #removeFile("./retmp/data/" & path & ".contents") - # And other used files / folder - # We may now remove the folder - #removeDir("./retmp/data/" & path) - - return doc - - -proc download*(path: string, preset: Preset): Flowvar[Document] = +import weave +import deques +import std/[os, osproc, options, json] +import .. /tablet/downloader + + +proc download*(path: string, preset: Preset): Flowvar[Document] = return spawn download_worker(path, preset) \ No newline at end of file