6 커밋 1b0a81c2a1 ... a7d5a2b99b

작성자 SHA1 메시지 날짜
  Slava Barinov a7d5a2b99b [Telegram] Migrated to LineFilter 1 년 전
  Slava Barinov 3427d0b286 [Main] Migrated to LineFilter 1 년 전
  Slava Barinov a158b92c82 [Categories] LineFilter added for chain-like filter creation 1 년 전
  Slava Barinov 52d9718742 [UI] Set ItemView column as a enum class 1 년 전
  Slava Barinov ba20012bfd [UI] Split into files logically 1 년 전
  Slava Barinov be7f764b09 [UI] UI window prepared 1 년 전
13개의 변경된 파일694개의 추가작업 그리고 287개의 파일을 삭제
  1. 6 0
      build.rs
  2. 19 4
      extern/Makefile
  3. 87 0
      extern/app.cpp
  4. 34 0
      extern/app.h
  5. 158 0
      extern/itemview.cpp
  6. 101 0
      extern/itemview.h
  7. 48 0
      extern/itemwindow.cpp
  8. 73 0
      extern/itemwindow.h
  9. 1 1
      extern/tvision
  10. 1 239
      extern/ui.cpp
  11. 138 0
      src/categories.rs
  12. 10 33
      src/main.rs
  13. 18 10
      src/telegram.rs

+ 6 - 0
build.rs

@@ -13,6 +13,9 @@ fn main() {
     #[cfg(feature = "tv")]
     {
         println!("cargo:rerun-if-changed=extern/ui.cpp");
+        println!("cargo:rerun-if-changed=extern/app.cpp");
+        println!("cargo:rerun-if-changed=extern/itemview.cpp");
+        println!("cargo:rerun-if-changed=extern/itemwindow.cpp");
         println!("cargo:rerun-if-changed=extern/tvision");
 
         Command::new("cmake")
@@ -40,6 +43,9 @@ fn main() {
         Build::new()
             .cpp(true)
             .file("extern/ui.cpp")
+            .file("extern/itemview.cpp")
+            .file("extern/itemwindow.cpp")
+            .file("extern/app.cpp")
             .flag("-Wno-unknown-pragmas")
             .flag("-Wno-reorder")
             .flag("-Wno-extra")

+ 19 - 4
extern/Makefile

@@ -1,6 +1,21 @@
-all:
-	g++ -O0 -g3 -I tvision/include/ -fPIE -DBINARY ui.cpp -c -o ui.o
-	g++ -O0 -g3 -L tvision/build -fPIE ui.o -Wl,-Bstatic -ltvision -Wl,-Bdynamic -lncursesw -ltinfow -lgpm -o ui
+CXX = g++
+COMMONFLAGS := -I tvision/include/ -std=c++23 -fPIE -Wall -Wextra -Werror -pedantic -Wno-unknown-pragmas
+#OPTFLAGS := -O0 -g3
+OPTFLAGS := -Ofast -flto -fvisibility=hidden
+LINKFLAGS := -L tvision/build -fPIE $(OPTFLAGS)
+LIBS := -Wl,-Bstatic -ltvision -Wl,-Bdynamic -lncursesw -ltinfow -lgpm
+SOURCES = $(wildcard *.cpp)
+
+OBJECTS = $(SOURCES:.cpp=.o)
+
+app: $(OBJECTS)
+	g++ $(OPTFLAGS) $(LINKFLAGS) $(OBJECTS) $(LIBS) -o ui
+
+%.o: %.cpp
+	$(CXX) -c $(COMMONFLAGS) $(OPTFLAGS) $< -o $@
+
+clean:
+	rm -f $(OBJECTS)
 
 format:
-	clang-format -i ui.cpp
+	clang-format -i *.h *.cpp

+ 87 - 0
extern/app.cpp

@@ -0,0 +1,87 @@
+#include "app.h"
+
+#define Uses_TDialog
+#define Uses_TKeys
+#define Uses_TDeskTop
+#define Uses_TButton
+#define Uses_TStaticText
+#define Uses_TStatusLine
+#define Uses_TStatusDef
+#define Uses_TStatusItem
+#define Uses_TSubMenu
+#define Uses_TMenuBar
+
+#include <tvision/tv.h>
+
+THelloApp::THelloApp()
+    : TProgInit(&THelloApp::initStatusLine, &THelloApp::initMenuBar,
+                &THelloApp::initDeskTop) {}
+
+void THelloApp::greetingBox() {
+  TDialog *d = new TDialog(TRect(25, 5, 55, 16), "Hello, World!");
+
+  d->insert(new TStaticText(TRect(3, 5, 15, 6), hello_line.c_str()));
+  d->insert(new TButton(TRect(16, 2, 28, 4), "Terrific", cmCancel, bfNormal));
+  d->insert(new TButton(TRect(16, 4, 28, 6), "Ok", cmCancel, bfNormal));
+  d->insert(new TButton(TRect(16, 6, 28, 8), "Lousy", cmCancel, bfNormal));
+  d->insert(new TButton(TRect(16, 8, 28, 10), "Cancel", cmCancel, bfNormal));
+
+  deskTop->execView(d);
+  destroy(d);
+}
+
+void THelloApp::handleEvent(TEvent &event) {
+  TApplication::handleEvent(event);
+  if (event.what == evCommand) {
+    switch (event.message.command) {
+    case GreetThemCmd:
+      greetingBox();
+      clearEvent(event);
+      break;
+    case CallListCmd:
+      if (TView *w = validView(new TItemWindow("test")))
+        deskTop->insert(w);
+      clearEvent(event);
+      break;
+    default:
+      break;
+    }
+  }
+}
+
+TMenuBar *THelloApp::initMenuBar(TRect r) {
+
+  r.b.y = r.a.y + 1;
+
+  return new TMenuBar(
+      r, *new TSubMenu("~F~ile", kbAltH) +
+             *new TMenuItem("~G~reeting...", GreetThemCmd, kbAltG) +
+             *new TMenuItem("~L~ist...", CallListCmd, kbAltL) + newLine() +
+             *new TMenuItem("E~x~it", cmQuit, cmQuit, hcNoContext, "Alt-X"));
+}
+
+char text[100] = "init";
+
+const char *THintStatusLine::hint(ushort) { return text; }
+
+TStatusLine *THelloApp::initStatusLine(TRect r) {
+  r.a.y = r.b.y - 1;
+
+  return new THintStatusLine(
+      r, *new TStatusDef(0, 50) +
+             *new TStatusItem("~F10~ Menu", kbF10, cmMenu) +
+             *new TStatusItem("~Alt-X~ Exit", kbAltX, cmQuit) +
+
+             *new TStatusDef(50, 0xFFFF) + *new TStatusItem(0, kbF10, cmMenu) +
+             *new TStatusItem("~F1~ Help", kbF1, cmHelp));
+}
+
+void THelloApp::idle() {
+  if (statusLine != 0)
+    statusLine->update();
+
+  if (commandSetChanged == True) {
+    message(this, evBroadcast, cmCommandSetChanged, 0);
+    commandSetChanged = False;
+  }
+}

+ 34 - 0
extern/app.h

@@ -0,0 +1,34 @@
+#pragma once
+
+#include "itemwindow.h"
+
+#define Uses_TApplication
+#define Uses_TEvent
+#define Uses_TRect
+
+#include <tvision/tv.h>
+
+const int GreetThemCmd = 100;
+const int CallListCmd = 101;
+
+class THelloApp : public TApplication {
+
+public:
+  THelloApp();
+
+  virtual void handleEvent(TEvent &event);
+  static TMenuBar *initMenuBar(TRect);
+  static TStatusLine *initStatusLine(TRect);
+  void set_hello(const std::string &new_line) { hello_line = new_line; }
+  virtual void idle() override;
+
+private:
+  std::string hello_line;
+  void greetingBox();
+};
+
+class THintStatusLine : public TStatusLine {
+public:
+  THintStatusLine(TRect r, TStatusDef &def) : TStatusLine(r, def) {}
+  virtual const char *hint(ushort) override;
+};

+ 158 - 0
extern/itemview.cpp

@@ -0,0 +1,158 @@
+#include "itemview.h"
+#include "tvision/include/tvision/ttypes.h"
+
+TPalette &TItemViewer::getPalette() const {
+  static TPalette palette(cpTestView, sizeof(cpTestView) - 1);
+  return palette;
+}
+
+const int maxLineLength = 256;
+
+const char *const TItemViewer::name = "TItemViewer";
+extern char text[100];
+
+void TItemViewer::handleEvent(TEvent &event) {
+  TScroller::handleEvent(event);
+
+  switch (event.what) {
+  case evKeyDown:
+    switch (event.keyDown.keyCode) {
+    case kbDown:
+      if (selectedLine < fileLines->getCount() - 1)
+        selectedLine++;
+      clearEvent(event);
+      break;
+    case kbUp:
+      if (selectedLine > 0)
+        selectedLine--;
+      clearEvent(event);
+      break;
+    }
+    break;
+  case evMouseDown:
+    TPoint mouse = makeLocal(event.mouse.where);
+    int i = findSel(mouse);
+    if (i != -1)
+      selectedLine = i;
+    clearEvent(event);
+    break;
+  }
+  sprintf(text, "%s: %d", fileName, selectedLine);
+  TProgram::application->statusLine->update();
+  draw();
+  TProgram::application->statusLine->draw();
+}
+
+int TItemViewer::findSel(TPoint p) {
+  TRect r = getExtent();
+  if (!r.contains(p))
+    return -1;
+  else {
+    int s = p.y - 2;
+    if (s >= fileLines->getCount())
+      return -1;
+    else
+      return s;
+  }
+}
+
+TItemViewer::TItemViewer(const TRect &bounds, TScrollBar *aHScrollBar,
+                         TScrollBar *aVScrollBar, const ViewedColumn &col)
+    : TScroller(bounds, aHScrollBar, aVScrollBar), column(col) {
+  if (col == ViewedColumn::Categories)
+    growMode = gfGrowHiX | gfGrowHiY;
+  else
+    growMode = gfGrowHiY;
+
+  isValid = True;
+  fileLines = new TItemCollection(5, 5);
+  switch (col) {
+  case ViewedColumn::Items:
+    fileName = newStr("Items");
+    break;
+  case ViewedColumn::Categories:
+    fileName = newStr("Categories");
+    break;
+  case ViewedColumn::Weights:
+    fileName = newStr("Weights");
+    break;
+  }
+
+  fileLines->insert(newStr("1"));
+  fileLines->insert(newStr("2"));
+  fileLines->insert(newStr("3"));
+  fileLines->insert(newStr("4"));
+  fileLines->insert(newStr("5"));
+  fileLines->insert(newStr("6"));
+  fileLines->insert(newStr("7"));
+}
+
+TItemViewer::~TItemViewer() { destroy(fileLines); }
+
+void TItemViewer::draw() {
+  TDrawBuffer b;
+  char *p;
+  TAttrPair cNormal = getColor(1);
+  TAttrPair cFrame = getColor(3);
+  TAttrPair cSelected = getColor(2);
+  TAttrPair cHeader = getColor(4);
+
+  if (getState(sfFocused)) {
+    setStyle(cHeader[0], slBold);
+    setStyle(cFrame[0], slBold);
+  }
+
+  // Header
+  b.moveChar(0, ' ', cNormal, size.x);
+  b.moveChar(size.x - 1, BOX_SINGLE_VERTICAL, cFrame, 1);
+  b.moveStr(0, fileName, getState(sfFocused) ? cHeader : cFrame);
+  writeBuf(0, 0, (short)size.x, 1, b);
+  // Line
+  b.moveChar(0, ' ', cFrame, size.x);
+  char line[128] = {0};
+  memset(line, BOX_SINGLE_HORIZONTAL, size.x - 1);
+  b.moveStr(0, line, cFrame);
+  b.moveChar(size.x - 1, BOX_CROSS_SINGLE, cFrame, 1);
+  writeBuf(0, 1, (short)size.x, 1, b);
+
+  for (short i = 0; i < size.y; i++) {
+    b.moveChar(0, ' ', cFrame, size.x);
+    b.moveChar(size.x - 1, BOX_SINGLE_VERTICAL, cFrame, 1);
+    if (delta.y + i < fileLines->getCount()) {
+      p = (char *)(fileLines->at(delta.y + i));
+      if (p)
+        b.moveStr(0, p, i == selectedLine ? cSelected : cNormal);
+    }
+    writeBuf(0, i + 2, (short)size.x, 1, b);
+  }
+}
+
+void TItemViewer::scrollDraw() {
+  TScroller::scrollDraw();
+  draw();
+}
+
+void TItemViewer::setState(ushort aState, Boolean enable) {
+  TScroller::setState(aState, enable);
+  if (enable && (aState & sfExposed))
+    setLimit(limit.x, limit.y);
+}
+
+Boolean TItemViewer::valid(ushort) const { return isValid; }
+
+void *TItemViewer::read(ipstream &is) {
+  char *fName = NULL;
+  TScroller::read(is);
+  delete fName;
+  return this;
+}
+
+void TItemViewer::write(opstream &os) {
+  TScroller::write(os);
+  os.writeString(fileName);
+}
+
+TStreamable *TItemViewer::build() { return new TItemViewer(streamableInit); }
+
+TStreamableClass RItemView(TItemViewer::name, TItemViewer::build,
+                           __DELTA(TItemViewer));

+ 101 - 0
extern/itemview.h

@@ -0,0 +1,101 @@
+#pragma once
+
+#define Uses_TWindow
+#define Uses_TCollection
+#define Uses_TScroller
+#define Uses_TEvent
+#define Uses_TKeys
+#define Uses_TProgram
+#define Uses_TStatusLine
+#define Uses_TStreamableClass
+#define Uses_opstream
+
+#include <tvision/tv.h>
+
+// Single Line Frame Characters
+const char BOX_SINGLE_HORIZONTAL = '\xC4'; // ─
+const char BOX_SINGLE_VERTICAL = '\xB3';   // │
+
+// Double Line Frame Characters
+const char BOX_DOUBLE_HORIZONTAL = '\xCD'; // ═
+const char BOX_DOUBLE_VERTICAL = '\xBA';   // ║
+
+// Junctions: Single Horizontal to Double Vertical
+const char BOX_SINGLE_HORIZONTAL_TO_DOUBLE_VERTICAL_TOP =
+    '\xD1'; // ╤ (Approximation)
+const char BOX_SINGLE_HORIZONTAL_TO_DOUBLE_VERTICAL_BOTTOM =
+    '\xCF'; // ╧ (Approximation)
+const char BOX_SINGLE_HORIZONTAL_TO_SINGLE_VERTICAL_TOP = '\xC2'; // ┬
+
+// Junctions: Double Horizontal to Single Vertical
+const char BOX_DOUBLE_HORIZONTAL_TO_SINGLE_VERTICAL_LEFT =
+    '\xC3'; // ╟ (Approximation)
+const char BOX_DOUBLE_HORIZONTAL_TO_SINGLE_VERTICAL_RIGHT =
+    '\xB4'; // ╢ (Approximation)
+
+// Cross junctions
+const char BOX_CROSS_SINGLE = '\xC5'; // ┼
+const char BOX_CROSS_DOUBLE =
+    '\xCE'; // ╬ (No direct equivalent, used closest match)
+const char BOX_CROSS_SINGLE_TO_DOUBLE = '\xD8'; // ╪ (Approximation)
+const char BOX_CROSS_DOUBLE_TO_SINGLE = '\xD7'; // ╫ (Approximation)
+
+// Corners
+const char BOX_CORNER_TOP_LEFT_SINGLE = '\xDA';     // ┌
+const char BOX_CORNER_TOP_RIGHT_SINGLE = '\xBF';    // ┐
+const char BOX_CORNER_BOTTOM_LEFT_SINGLE = '\xC0';  // └
+const char BOX_CORNER_BOTTOM_RIGHT_SINGLE = '\xD9'; // ┘
+
+const char BOX_CORNER_TOP_LEFT_DOUBLE = '\xC9';     // ╔
+const char BOX_CORNER_TOP_RIGHT_DOUBLE = '\xBB';    // ╗
+const char BOX_CORNER_BOTTOM_LEFT_DOUBLE = '\xC8';  // ╚
+const char BOX_CORNER_BOTTOM_RIGHT_DOUBLE = '\xBC'; // ╝
+
+class TItemCollection : public TCollection {
+
+public:
+  TItemCollection(short lim, short delta) : TCollection(lim, delta) {}
+  virtual void freeItem(void *p) { delete[] (char *)p; }
+
+private:
+  virtual void *readItem(ipstream &) { return 0; }
+  virtual void writeItem(void *, opstream &) {}
+};
+
+class TItemViewer : public TScroller {
+
+  int selectedLine = 0;
+
+public:
+  enum class ViewedColumn { Items, Categories, Weights };
+
+  ViewedColumn column;
+  char *fileName;
+  TCollection *fileLines;
+  Boolean isValid;
+  TItemViewer(const TRect &bounds, TScrollBar *aHScrollBar,
+              TScrollBar *aVScrollBar,
+              const ViewedColumn &col = ViewedColumn::Items);
+  ~TItemViewer();
+  TItemViewer(StreamableInit) : TScroller(streamableInit){};
+  void draw();
+  void setState(ushort aState, Boolean enable);
+  void scrollDraw();
+  Boolean valid(ushort command) const;
+  virtual void handleEvent(TEvent &event) override;
+  int findSel(TPoint p);
+  virtual TPalette &getPalette() const override;
+
+private:
+  virtual const char *streamableName() const { return name; }
+
+protected:
+  virtual void write(opstream &);
+  virtual void *read(ipstream &);
+
+public:
+  static const char *const name;
+  static TStreamable *build();
+};
+
+#define cpTestView "\x6\x7\x2\x9"

+ 48 - 0
extern/itemwindow.cpp

@@ -0,0 +1,48 @@
+#include "itemwindow.h"
+
+#define Uses_TProgram
+#define Uses_TDeskTop
+
+#include <tvision/tv.h>
+
+static short winNumber = 0;
+
+TFrame *TItemWindow::initFrame(TRect r) { return new TCustomFrame(r); }
+
+TItemWindow::TItemWindow(const char *fileName)
+    : TWindowInit(&TItemWindow::initFrame),
+      TWindow(TProgram::deskTop->getExtent(), fileName, winNumber++) {
+  options |= ofTileable;
+
+  itemViewer = new TItemViewer(
+      itemViewerBounds(), standardScrollBar(sbHorizontal | sbHandleKeyboard),
+      standardScrollBar(sbVertical | sbHandleKeyboard),
+      TItemViewer::ViewedColumn::Items);
+  insert(itemViewer);
+
+  catViewer = new TItemViewer(
+      catViewerBounds(), standardScrollBar(sbHorizontal | sbHandleKeyboard),
+      standardScrollBar(sbVertical | sbHandleKeyboard),
+      TItemViewer::ViewedColumn::Categories);
+  insert(catViewer);
+
+  weightViewer = new TItemViewer(
+      weightViewerBounds(), standardScrollBar(sbHorizontal | sbHandleKeyboard),
+      standardScrollBar(sbVertical | sbHandleKeyboard),
+      TItemViewer::ViewedColumn::Weights);
+  insert(weightViewer);
+}
+
+void TItemWindow::draw() {
+  itemViewer->changeBounds(itemViewerBounds());
+  catViewer->changeBounds(catViewerBounds());
+  weightViewer->changeBounds(weightViewerBounds());
+  TWindow::draw();
+}
+
+void TItemWindow::handleEvent(TEvent &event) { TWindow::handleEvent(event); }
+
+TPalette &TItemWindow::getPalette() const {
+  static TPalette palette(cpItemWindow, sizeof(cpItemWindow) - 1);
+  return palette;
+}

+ 73 - 0
extern/itemwindow.h

@@ -0,0 +1,73 @@
+#pragma once
+
+#include "itemview.h"
+
+#define Uses_TWindow
+#define Uses_TRect
+#define Uses_TEvent
+#define Uses_TFrame
+
+#include <tvision/tv.h>
+
+class TItemWindow : public TWindow {
+  TItemViewer *itemViewer, *catViewer, *weightViewer;
+  const TRect itemViewerBounds() const {
+    auto bounds = getExtent();
+    TRect r(bounds.a.x, bounds.a.y, bounds.b.x / 3, bounds.b.y);
+    r.grow(-1, -1);
+    return r;
+  }
+
+  const TRect catViewerBounds() const {
+    auto bounds = getExtent();
+    TRect r(bounds.b.x / 3 - 2, bounds.a.y, 2 * bounds.b.x / 3 - 1, bounds.b.y);
+    r.grow(-1, -1);
+    return r;
+  }
+
+  const TRect weightViewerBounds() const {
+    auto bounds = getExtent();
+    TRect r(2 * bounds.b.x / 3 - 3, bounds.a.y, bounds.b.x, bounds.b.y);
+    r.grow(-1, -1);
+    return r;
+  }
+
+public:
+  TItemWindow(const char *fileName);
+  static TFrame *initFrame(TRect r);
+  void draw() override;
+  virtual void handleEvent(TEvent &event) override;
+  virtual TPalette &getPalette() const override;
+
+protected:
+  virtual void sizeLimits(TPoint &min, TPoint &max) override {
+    TWindow::sizeLimits(min, max);
+    min.x = size.x / 2 + 10;
+  };
+};
+
+class TCustomFrame : public TFrame {
+public:
+  using TFrame::TFrame; // Inherit constructor
+
+  virtual void draw() override {
+    TFrame::draw(); // Call base class draw
+    TDrawBuffer b;
+    TAttrPair cNormal = getState(sfActive) ? getColor(3) : getColor(1);
+
+    if (getState(sfDragging))
+      cNormal = getColor(5);
+
+    int fstSep = size.x / 3 - 2;
+    int sndSep = 2 * size.x / 3 - 3;
+
+    b.moveChar(0, ' ', cNormal, 1);
+    b.putChar(0, (getState(sfActive) && !getState(sfDragging))
+                     ? BOX_SINGLE_HORIZONTAL_TO_DOUBLE_VERTICAL_TOP
+                     : BOX_SINGLE_HORIZONTAL_TO_SINGLE_VERTICAL_TOP);
+    writeLine(fstSep, 0, 1, 1, b);
+    writeLine(sndSep, 0, 1, 1, b);
+  }
+};
+
+#define cpItemWindow "\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x06"

+ 1 - 1
extern/tvision

@@ -1 +1 @@
-Subproject commit c36e190174463ece471bdd1c06959fb8dc343c3c
+Subproject commit d1fa783e0fa8685c199563a466cdc221e8d9b85c

+ 1 - 239
extern/ui.cpp

@@ -1,244 +1,6 @@
-#define Uses_TKeys
-#define Uses_TApplication
-#define Uses_TEvent
-#define Uses_TRect
-#define Uses_TDialog
-#define Uses_TStaticText
-#define Uses_TButton
-#define Uses_TMenuBar
-#define Uses_TSubMenu
-#define Uses_TMenuItem
-#define Uses_TStatusLine
-#define Uses_TStatusItem
-#define Uses_TStatusDef
-#define Uses_TScroller
-#define Uses_TDeskTop
-#define Uses_TCollection
-#define Uses_TScroller
-#define Uses_TWindow
-#define Uses_ipstream
-#define Uses_opstream
-#define Uses_TStreamableClass
+#include "app.h"
 
-#include <string>
-#include <tvision/tv.h>
-
-class TItemCollection : public TCollection {
-
-public:
-  TItemCollection(short lim, short delta) : TCollection(lim, delta) {}
-  virtual void freeItem(void *p) { delete[](char *) p; }
-
-private:
-  virtual void *readItem(ipstream &) { return 0; }
-  virtual void writeItem(void *, opstream &) {}
-};
-
-class TItemViewer : public TScroller {
-
-public:
-  char *fileName;
-  TCollection *fileLines;
-  Boolean isValid;
-  TItemViewer(const TRect &bounds, TScrollBar *aHScrollBar,
-              TScrollBar *aVScrollBar, Boolean left);
-  ~TItemViewer();
-  TItemViewer(StreamableInit) : TScroller(streamableInit){};
-  void draw();
-  void setState(ushort aState, Boolean enable);
-  void scrollDraw();
-  Boolean valid(ushort command) const;
-
-private:
-  virtual const char *streamableName() const { return name; }
-
-protected:
-  virtual void write(opstream &);
-  virtual void *read(ipstream &);
-
-public:
-  static const char *const name;
-  static TStreamable *build();
-};
-
-class TItemWindow : public TWindow {
-
-public:
-  TItemWindow(const char *fileName);
-protected:
-  virtual void sizeLimits(TPoint& min, TPoint& max) override {
-    TWindow::sizeLimits(min, max);
-    min.x = size.x / 2 + 10;
-  };
-
-};
-
-const int maxLineLength = 256;
-
-const char *const TItemViewer::name = "TItemViewer";
-
-TItemViewer::TItemViewer(const TRect &bounds, TScrollBar *aHScrollBar,
-                         TScrollBar *aVScrollBar, Boolean left)
-    : TScroller(bounds, aHScrollBar, aVScrollBar) {
-  if (left)
-    growMode = gfGrowHiY;
-  else
-    growMode = gfGrowHiX | gfGrowHiY;
-
-  isValid = True;
-  fileName = 0;
-  fileLines = new TItemCollection(5, 5);
-  fileLines->insert(newStr(left ? "Items" : "Categories"));
-}
-
-TItemViewer::~TItemViewer() { destroy(fileLines); }
-
-void TItemViewer::draw() {
-  char *p;
-
-  ushort c = getColor(0x0301);
-
-  for (short i = 0; i < size.y; i++) {
-    TDrawBuffer b;
-    b.moveChar(0, ' ', c, size.x);
-
-    if (delta.y + i < fileLines->getCount()) {
-      p = (char *)(fileLines->at(delta.y + i));
-      if (p)
-        b.moveStr(0, p, c, (short)size.x, (short)delta.x);
-    }
-    writeBuf(0, i, (short)size.x, 1, b);
-  }
-}
-
-void TItemViewer::scrollDraw() {
-  TScroller::scrollDraw();
-  draw();
-}
-
-void TItemViewer::setState(ushort aState, Boolean enable) {
-  TScroller::setState(aState, enable);
-  if (enable && (aState & sfExposed))
-    setLimit(limit.x, limit.y);
-}
-
-Boolean TItemViewer::valid(ushort) const { return isValid; }
-
-void *TItemViewer::read(ipstream &is) {
-  char *fName = NULL;
-  TScroller::read(is);
-  delete fName;
-  return this;
-}
-
-void TItemViewer::write(opstream &os) {
-  TScroller::write(os);
-  os.writeString(fileName);
-}
-
-TStreamable *TItemViewer::build() { return new TItemViewer(streamableInit); }
-
-TStreamableClass RItemView(TItemViewer::name, TItemViewer::build,
-                           __DELTA(TItemViewer));
-
-static short winNumber = 0;
-
-TItemWindow::TItemWindow(const char *fileName)
-    : TWindow(TProgram::deskTop->getExtent(), fileName, winNumber++),
-      TWindowInit(&TItemWindow::initFrame) {
-  options |= ofTileable;
-  auto bounds = getExtent();
-  TRect r(bounds.a.x, bounds.a.y, bounds.b.x / 2 + 1, bounds.b.y);
-  r.grow(-1, -1);
-  insert(new TItemViewer(r, standardScrollBar(sbHorizontal | sbHandleKeyboard),
-                         standardScrollBar(sbVertical | sbHandleKeyboard),
-                         True));
-  r = TRect(bounds.b.x / 2, bounds.a.y, bounds.b.x, bounds.b.y);
-  r.grow(-1, -1);
-  insert(new TItemViewer(r, standardScrollBar(sbHorizontal | sbHandleKeyboard),
-                         standardScrollBar(sbVertical | sbHandleKeyboard),
-                         False));
-}
-
-const int GreetThemCmd = 100;
-const int CallListCmd = 101;
-
-class THelloApp : public TApplication {
-
-public:
-  THelloApp();
-
-  virtual void handleEvent(TEvent &event);
-  static TMenuBar *initMenuBar(TRect);
-  static TStatusLine *initStatusLine(TRect);
-  void set_hello(const std::string &new_line) { hello_line = new_line; }
-
-private:
-  std::string hello_line;
-  void greetingBox();
-};
-
-THelloApp::THelloApp()
-    : TProgInit(&THelloApp::initStatusLine, &THelloApp::initMenuBar,
-                &THelloApp::initDeskTop) {}
-
-void THelloApp::greetingBox() {
-  TDialog *d = new TDialog(TRect(25, 5, 55, 16), "Hello, World!");
-
-  d->insert(new TStaticText(TRect(3, 5, 15, 6), hello_line.c_str()));
-  d->insert(new TButton(TRect(16, 2, 28, 4), "Terrific", cmCancel, bfNormal));
-  d->insert(new TButton(TRect(16, 4, 28, 6), "Ok", cmCancel, bfNormal));
-  d->insert(new TButton(TRect(16, 6, 28, 8), "Lousy", cmCancel, bfNormal));
-  d->insert(new TButton(TRect(16, 8, 28, 10), "Cancel", cmCancel, bfNormal));
-
-  deskTop->execView(d);
-  destroy(d);
-}
-
-void THelloApp::handleEvent(TEvent &event) {
-  TApplication::handleEvent(event);
-  if (event.what == evCommand) {
-    switch (event.message.command) {
-    case GreetThemCmd:
-      greetingBox();
-      clearEvent(event);
-      break;
-    case CallListCmd:
-      if (TView *w = validView(new TItemWindow("test")))
-        deskTop->insert(w);
-      clearEvent(event);
-      break;
-    default:
-      break;
-    }
-  }
-}
-
-TMenuBar *THelloApp::initMenuBar(TRect r) {
-
-  r.b.y = r.a.y + 1;
-
-  return new TMenuBar(
-      r, *new TSubMenu("~F~ile", kbAltH) +
-             *new TMenuItem("~G~reeting...", GreetThemCmd, kbAltG) +
-             *new TMenuItem("~L~ist...", CallListCmd, kbAltL) + newLine() +
-             *new TMenuItem("E~x~it", cmQuit, cmQuit, hcNoContext, "Alt-X"));
-}
-
-TStatusLine *THelloApp::initStatusLine(TRect r) {
-  r.a.y = r.b.y - 1;
-  return new TStatusLine(r,
-                         *new TStatusDef(0, 0xFFFF) +
-                             *new TStatusItem("~Alt-X~ Exit", kbAltX, cmQuit) +
-                             *new TStatusItem(0, kbF10, cmMenu));
-}
-
-#ifndef BINARY
 extern "C" int ui_main(char *hello_line) {
-#else
-int main() {
-  const char *hello_line = "test line";
-#endif
   THelloApp helloWorld;
   helloWorld.set_hello(hello_line);
   helloWorld.run();

+ 138 - 0
src/categories.rs

@@ -102,6 +102,78 @@ pub fn get_category(item: &str, storage: &mut CatStats, accounts: &HashSet<Strin
     }
 }
 
+pub struct LineFilter<F>
+where
+    F: Fn(&str) -> &str,
+{
+    filter: F,
+}
+
+impl LineFilter<fn(&str) -> &str> {
+    pub fn new() -> Self {
+        Self {
+            filter: |input| input,
+        }
+    }
+}
+
+impl<F> LineFilter<F>
+where
+    F: Fn(&str) -> &str,
+{
+    pub fn numfilter(self) -> LineFilter<impl Fn(&str) -> &str> {
+        LineFilter {
+            filter: move |input| {
+                let intermediate = (self.filter)(input);
+                intermediate
+                    .trim_start()
+                    .trim_start_matches(char::is_numeric)
+                    .trim_start()
+            },
+        }
+    }
+
+    pub fn perekrestok_filter(self) -> LineFilter<impl Fn(&str) -> &str> {
+        LineFilter {
+            filter: move |input| {
+                let intermediate = (self.filter)(input);
+                intermediate
+                    .trim_start()
+                    .trim_start_matches(char::is_numeric)
+                    .trim_start_matches(['*', ':', ' '])
+                    .trim_start()
+            },
+        }
+    }
+
+    pub fn trim_units_from_end(self) -> LineFilter<impl Fn(&str) -> &str> {
+        LineFilter {
+            filter: move |input| {
+                let intermediate = (self.filter)(input);
+                let units = ["кг", "г", "мл", "л"];
+
+                let mut trimmed = intermediate;
+
+                for unit in &units {
+                    if trimmed.ends_with(unit) {
+                        trimmed = trimmed.trim_end_matches(unit).trim_end();
+                        break;
+                    }
+                }
+
+                // Trim any numeric characters at the end.
+                trimmed = trimmed.trim_end_matches(char::is_numeric).trim_end();
+
+                trimmed
+            },
+        }
+    }
+
+    pub fn build(self) -> impl Fn(&str) -> &str {
+        self.filter
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
@@ -135,4 +207,70 @@ mod tests {
         let topcat = get_top_category("item", &cm).unwrap();
         assert_eq!(topcat, "category");
     }
+
+    #[test]
+    fn test_new() {
+        let filter = LineFilter::new();
+        assert_eq!(filter.build()("Hello"), "Hello");
+    }
+
+    #[test]
+    fn test_numfilter() {
+        let filter = LineFilter::new().numfilter();
+        assert_eq!(filter.build()("123Hello"), "Hello");
+    }
+
+    #[test]
+    fn test_perekrestok_filter() {
+        let filter = LineFilter::new().perekrestok_filter();
+        assert_eq!(filter.build()("123: *Hello"), "Hello");
+    }
+
+    #[test]
+    fn test_chaining() {
+        let filter = LineFilter::new().numfilter().perekrestok_filter();
+        assert_eq!(filter.build()("123: *Hello"), "Hello");
+    }
+
+    #[test]
+    fn test_trim_no_unit() {
+        let filter = LineFilter::new().trim_units_from_end().build();
+        assert_eq!(filter("Apple Juice"), "Apple Juice");
+    }
+
+    #[test]
+    fn test_trim_kg() {
+        let filter = LineFilter::new().trim_units_from_end().build();
+        assert_eq!(filter("Oranges 2кг"), "Oranges");
+    }
+
+    #[test]
+    fn test_trim_g() {
+        let filter = LineFilter::new().trim_units_from_end().build();
+        assert_eq!(filter("Salt 500 г"), "Salt");
+    }
+
+    #[test]
+    fn test_trim_ml() {
+        let filter = LineFilter::new().trim_units_from_end().build();
+        assert_eq!(filter("Water 150мл"), "Water");
+    }
+
+    #[test]
+    fn test_trim_l() {
+        let filter = LineFilter::new().trim_units_from_end().build();
+        assert_eq!(filter("Milk 2 л"), "Milk");
+    }
+
+    #[test]
+    fn test_trim_multiple_spaces() {
+        let filter = LineFilter::new().trim_units_from_end().build();
+        assert_eq!(filter("Honey   100  г"), "Honey");
+    }
+
+    #[test]
+    fn test_trim_with_other_text() {
+        let filter = LineFilter::new().trim_units_from_end().build();
+        assert_eq!(filter("Bread 300г Extra"), "Bread 300г Extra");
+    }
 }

+ 10 - 33
src/main.rs

@@ -29,14 +29,6 @@ struct Cli {
     #[structopt(long, default_value = "New")]
     memo: String,
 
-    /// Add filter with cutting id from every item memo beginning
-    #[structopt(long)]
-    numfilt: bool,
-
-    /// Add filter with cutting "3*:" and numbers from the line (e.g. Perekrestok)
-    #[structopt(long)]
-    perfilt: bool,
-
     /// Run telegram bot
     #[cfg(feature = "telegram")]
     #[structopt(short, long)]
@@ -60,24 +52,6 @@ struct Cli {
     account_type: AccountType,
 }
 
-fn numfilter(line: &str) -> &str {
-    line.trim_start()
-        .trim_start_matches(char::is_numeric)
-        .trim_start()
-}
-
-fn perekrestok_filter(line: &str) -> &str {
-    numfilter(
-        line.trim_start()
-            .trim_start_matches(char::is_numeric)
-            .trim_start_matches(['*', ':', ' ']),
-    )
-}
-
-fn nofilter(line: &str) -> &str {
-    line
-}
-
 #[cfg(not(tarpaulin_include))]
 fn main() {
     pretty_env_logger::init();
@@ -104,12 +78,11 @@ fn main() {
         return;
     }
 
-    let filter = if args.numfilt { numfilter } else { nofilter };
-    let filter = if args.perfilt {
-        perekrestok_filter
-    } else {
-        filter
-    };
+    let filter = categories::LineFilter::new()
+        .numfilter()
+        .perekrestok_filter()
+        .trim_units_from_end()
+        .build();
 
     // If program is used as command-line tool
     let acc = Account::new()
@@ -118,10 +91,14 @@ fn main() {
         .build();
 
     if let Some(filename) = &args.filename {
+        let cat_filter = categories::LineFilter::new()
+            .numfilter()
+            .perekrestok_filter()
+            .build();
         let cat = &|item: &str,
                     stats: &mut categories::CatStats,
                     acc: &HashSet<String>|
-         -> String { categories::get_category(filter(item), stats, acc) };
+         -> String { categories::get_category(cat_filter(item), stats, acc) };
         let t = convert::convert(filename, &args.memo, &mut user, &acc, filter, cat).unwrap();
         print!("{}", acc);
         println!("{}", t);

+ 18 - 10
src/telegram.rs

@@ -316,8 +316,8 @@ fn create_categories_keyboard(catitems: &HashMap<String, String>) -> InlineKeybo
     let mut keyboard = InlineKeyboardMarkup::default(); // Use default to initialize
     let mut buttons = Vec::new();
 
-    for (index, (item, category)) in catitems.iter().enumerate() {
-        let button_text = format!("{}. {}", index + 1, category);
+    for (item, category) in catitems.iter() {
+        let button_text = format!("{}: {}", item, category);
         let callback_data = format!("edit_{}", item); // Assuming `item` is a unique identifier
 
         // Create a button and push it to the row
@@ -412,7 +412,7 @@ async fn handle_json(
             log::info!("Automatically categorized");
             bot.send_message(
                 msg.chat.id,
-                format!("Items are categorized and categories are updated"),
+                "Items are categorized and categories are updated".to_string(),
             )
             .reply_markup(create_categories_keyboard(&cat))
             .await?;
@@ -615,10 +615,6 @@ async fn handle_subcategory(
     Ok(())
 }
 
-fn nofilter(line: &str) -> &str {
-    line
-}
-
 async fn handle_qif_ready(
     bot: Bot,
     dialogue: QIFDialogue,
@@ -671,7 +667,13 @@ async fn handle_qif_ready(
         item_categories.get(item).unwrap().to_owned()
     };
 
-    let t = convert(&filename, memo, &mut user, &acc, nofilter, cat).unwrap();
+    let filter = categories::LineFilter::new()
+        .numfilter()
+        .perekrestok_filter()
+        .trim_units_from_end()
+        .build();
+
+    let t = convert(&filename, memo, &mut user, &acc, filter, cat).unwrap();
     let qif = InputFile::memory(format!("{}{}", acc, t).into_bytes());
     bot.send_message(msg.chat.id, "QIF is ready.").await?;
     bot.send_document(msg.chat.id, qif).await?;
@@ -690,9 +692,15 @@ async fn callback_handler(q: CallbackQuery, bot: Bot, dialogue: QIFDialogue) ->
         if version.starts_with("edit_") {
             let item_id = version.strip_prefix("edit_").unwrap(); // Extract the item ID or number
 
+            dialogue
+                .update(State::NewJson {
+                    filename: String::new(),
+                })
+                .await?;
+
             // Process the selection, e.g., by updating the dialogue state or responding to the user
             let response_message = format!("Editing item {}", item_id);
-            if let Some(chat_id) = q.message.clone().and_then(|msg| Some(msg.chat.id)) {
+            if let Some(chat_id) = q.message.clone().map(|msg| msg.chat.id) {
                 bot.send_message(chat_id, response_message).await?;
             }
         }
@@ -739,7 +747,7 @@ async fn callback_handler(q: CallbackQuery, bot: Bot, dialogue: QIFDialogue) ->
                                 .await?;
                             bot.send_message(
                                 chat.id,
-                                format!("Items are categorized and categories are updated"),
+                                "Items are categorized and categories are updated".to_string(),
                             )
                             .reply_markup(create_categories_keyboard(&items_processed))
                             .await?;