Browse Source

feat: implement wrapper for Window
feat: implement wrapper for Color
feat: implement wrapper for attron/attroff
feat: implement CLI processing

Sam Jaffe 1 year ago
parent
commit
cbbdb7346e

+ 32 - 0
include/ncurses-wrapper/attribute_scope.h

@@ -0,0 +1,32 @@
+//
+//  attribute_scope.hpp
+//  ncurses-wrapper
+//
+//  Created by Sam Jaffe on 7/23/24.
+//
+
+#pragma once
+
+#include <vector>
+
+#include <ncurses-wrapper/forward.h>
+
+namespace curses {
+class AttributeScope {
+private:
+  Window *window_{nullptr};
+  std::vector<NCURSES_ATTR_T> self_{};
+  bool good_{true};
+  
+public:
+  AttributeScope() = default;
+  AttributeScope(std::vector<NCURSES_ATTR_T> init, Window &window);
+  AttributeScope(AttributeScope const &) = delete;
+  AttributeScope(AttributeScope &&other) = default;
+  AttributeScope &operator=(AttributeScope const &) = delete;
+  AttributeScope &operator=(AttributeScope &&other) = default;
+  ~AttributeScope();
+  
+  operator bool() const { return good_; }
+};
+}

+ 35 - 0
include/ncurses-wrapper/bounds.h

@@ -0,0 +1,35 @@
+//
+//  Bounds.h
+//  ncurses-wrapper
+//
+//  Created by Sam Jaffe on 7/23/24.
+//
+
+#pragma once
+
+namespace curses {
+struct Bounds {
+  int width;
+  int height;
+};
+
+struct Offset {
+  int x;
+  int y;
+};
+
+struct Position {
+  friend Position operator+(Position const &lhs, Offset const &rhs) {
+    return {lhs.x + rhs.x, lhs.y + rhs.y};
+  }
+  friend Position operator-(Position const &lhs, Offset const &rhs) {
+    return {lhs.x - rhs.x, lhs.y - rhs.y};
+  }
+  
+  Position only_x() const { return {x, 0}; }
+  Position only_y() const { return {0, y}; }
+
+  int x;
+  int y;
+};
+}

+ 36 - 0
include/ncurses-wrapper/cli.h

@@ -0,0 +1,36 @@
+//
+//  cli.hpp
+//  ncurses-wrapper
+//
+//  Created by Sam Jaffe on 7/23/24.
+//
+
+#pragma once
+
+#include <string>
+#include <vector>
+
+#include <ncurses-wrapper/forward.h>
+#include <ncurses-wrapper/window.h>
+
+namespace curses {
+class Cli {
+private:
+  Window window_;
+  std::string prompt_;
+  std::vector<std::string> stack_{""};
+  int horizontal_offset_{0};
+  int vertical_offset_{0};
+
+public:
+  template <typename... Flags>
+  explicit Cli(std::string const &prompt, Flags const &...flags)
+      : window_(flags..., NoEcho, ExtendedKeys), prompt_(prompt) {}
+  
+  void loop(std::function<void(curses::Window &, std::string)> on_enter);
+  
+private:
+  size_t index() const { return stack_.size() - vertical_offset_ - 1; }
+  std::string &get() { return stack_.at(index()); }
+};
+}

+ 40 - 0
include/ncurses-wrapper/color.h

@@ -0,0 +1,40 @@
+//
+//  color.h
+//  ncurses-wrapper
+//
+//  Created by Sam Jaffe on 7/23/24.
+//
+
+#include <compare>
+
+namespace curses {
+struct Color {
+  // https://www.rapidtables.com/web/color/RGB_Color.html
+  static Color const DEFAULT;
+  static Color const BLACK;
+  static Color const RED;
+  static Color const GREEN;
+  static Color const YELLOW;
+  static Color const BLUE;
+  static Color const MAGENTA;
+  static Color const CYAN;
+  static Color const WHITE;
+  
+  auto operator<=>(Color const &) const = default;
+  
+  static Color from_code(short code);
+  short to_code() const;
+
+  short red;
+  short green;
+  short blue;
+};
+
+struct ColorPair {
+  auto operator<=>(ColorPair const &) const = default;
+  int to_code() const;
+  
+  Color foreground;
+  Color background;
+};
+}

+ 31 - 0
include/ncurses-wrapper/forward.h

@@ -0,0 +1,31 @@
+//
+//  forward.h
+//  ncurses-wrapper
+//
+//  Created by Sam Jaffe on 7/23/24.
+//
+
+#pragma once
+
+#include <ncurses.h>
+
+namespace curses {
+enum class Status {
+  Ok, Error,
+};
+
+inline Status to_status(int code) {
+  return code == ERR ? Status::Error : Status::Ok;
+}
+
+struct Position;
+struct Offset;
+struct Bounds;
+
+struct Color;
+struct ColorPair;
+
+class AttributeScope;
+class Cli;
+class Window;
+}

+ 86 - 0
include/ncurses-wrapper/window.h

@@ -0,0 +1,86 @@
+//
+//  window.h
+//  ncurses-wrapper
+//
+//  Created by Sam Jaffe on 7/23/24.
+//
+
+#pragma once
+
+#include <sstream>
+#include <string>
+
+#include <ncurses-wrapper/attribute_scope.h>
+#include <ncurses-wrapper/bounds.h>
+#include <ncurses-wrapper/forward.h>
+
+#undef getch
+#undef delch
+
+namespace curses {
+constexpr struct Bold_t {} Bold;
+
+constexpr struct NoEcho_t {} NoEcho; // Don't automatically print input characters
+constexpr struct WithColor_t {} WithColor;
+constexpr struct ExtendedKeys_t {} ExtendedKeys; // Enable KEY_* keys as unique items
+
+class Window {
+private:
+  friend class AttributeScope;
+  WINDOW *self_;
+  
+public:
+  template <typename... Flags>
+  Window(Flags const &...flags) : Window(initscr(), flags...) {}
+  
+  template <typename... Flags>
+  Window(Bounds const &term, Flags const &...flags)
+      : Window(newwin(term.height, term.width, 0, 0), flags...) {}
+  
+  ~Window();
+    
+  void move(Position const &pos);
+  void move(Offset const &off);
+  Position getpos();
+  Bounds getsize();
+
+  int getch();
+  int delch();
+
+  void clear_line();
+  void clear();
+  
+  Status printf(char const *fmt, ...) GCC_PRINTFLIKE(2, 3);
+
+  template <typename... Attrs> AttributeScope with(Attrs const &...attrs) {
+    return {{to_attr(attrs...)}, *this};
+  }
+  
+private:
+  template <typename... Flags>
+  Window(WINDOW *win, Flags const &...flags) : self_(win) {
+    clear();
+    [[maybe_unused]] int _[] = {(init_with(flags), 0)...};
+  }
+
+  void init_with(NoEcho_t);
+  void init_with(WithColor_t);
+  void init_with(ExtendedKeys_t);
+  
+  NCURSES_ATTR_T to_attr(Bold_t) const;
+  NCURSES_ATTR_T to_attr(ColorPair const &color) const;
+};
+
+template <typename T>
+Window &operator<<(Window &os, T const &value) {
+  std::stringstream ss;
+  ss << value;
+  os.printf("%s", ss.str().c_str());
+  return os;
+}
+
+inline Window &operator<<(Window &os, char const *str) {
+  os.printf("%s", str);
+  return os;
+}
+}

+ 89 - 2
ncurses-wrapper.xcodeproj/project.pbxproj

@@ -6,8 +6,36 @@
 	objectVersion = 55;
 	objects = {
 
+/* Begin PBXBuildFile section */
+		CD93A7DA2C50160900754263 /* window.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD93A7D82C50160900754263 /* window.cxx */; };
+		CD93A7DD2C50161700754263 /* window.h in Headers */ = {isa = PBXBuildFile; fileRef = CD93A7DC2C50161700754263 /* window.h */; };
+		CD93A7DF2C50165300754263 /* bounds.h in Headers */ = {isa = PBXBuildFile; fileRef = CD93A7DE2C50165300754263 /* bounds.h */; };
+		CD93A7E22C5016A500754263 /* libncurses.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = CD93A7E12C5016A500754263 /* libncurses.tbd */; };
+		CD93A7E42C5016F400754263 /* forward.h in Headers */ = {isa = PBXBuildFile; fileRef = CD93A7E32C5016F400754263 /* forward.h */; };
+		CD93A7E72C501C0400754263 /* attribute_scope.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD93A7E52C501C0400754263 /* attribute_scope.cxx */; };
+		CD93A7E82C501C0400754263 /* attribute_scope.h in Headers */ = {isa = PBXBuildFile; fileRef = CD93A7E62C501C0400754263 /* attribute_scope.h */; };
+		CD93A7EB2C501F6F00754263 /* color.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CD93A7E92C501F6F00754263 /* color.cxx */; };
+		CD93A7EC2C501F6F00754263 /* color.h in Headers */ = {isa = PBXBuildFile; fileRef = CD93A7EA2C501F6F00754263 /* color.h */; };
+		CDEECC472C5050FD000C4392 /* libcurses.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = CDEECC462C5050FD000C4392 /* libcurses.tbd */; };
+		CDEECC4E2C5069C0000C4392 /* cli.cxx in Sources */ = {isa = PBXBuildFile; fileRef = CDEECC4C2C5069C0000C4392 /* cli.cxx */; };
+		CDEECC4F2C5069C0000C4392 /* cli.h in Headers */ = {isa = PBXBuildFile; fileRef = CDEECC4D2C5069C0000C4392 /* cli.h */; };
+/* End PBXBuildFile section */
+
 /* Begin PBXFileReference section */
 		CD93A7CD2C5015C000754263 /* libncurses-wrapper.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libncurses-wrapper.a"; sourceTree = BUILT_PRODUCTS_DIR; };
+		CD93A7D52C5015EF00754263 /* ncurses-wrapper */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "ncurses-wrapper"; path = "include/ncurses-wrapper"; sourceTree = "<group>"; };
+		CD93A7D82C50160900754263 /* window.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = window.cxx; sourceTree = "<group>"; };
+		CD93A7DC2C50161700754263 /* window.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = window.h; sourceTree = "<group>"; };
+		CD93A7DE2C50165300754263 /* bounds.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bounds.h; sourceTree = "<group>"; };
+		CD93A7E12C5016A500754263 /* libncurses.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libncurses.tbd; path = usr/lib/libncurses.tbd; sourceTree = SDKROOT; };
+		CD93A7E32C5016F400754263 /* forward.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = forward.h; sourceTree = "<group>"; };
+		CD93A7E52C501C0400754263 /* attribute_scope.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = attribute_scope.cxx; sourceTree = "<group>"; };
+		CD93A7E62C501C0400754263 /* attribute_scope.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = attribute_scope.h; sourceTree = "<group>"; };
+		CD93A7E92C501F6F00754263 /* color.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = color.cxx; sourceTree = "<group>"; };
+		CD93A7EA2C501F6F00754263 /* color.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = color.h; sourceTree = "<group>"; };
+		CDEECC462C5050FD000C4392 /* libcurses.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libcurses.tbd; path = usr/lib/libcurses.tbd; sourceTree = SDKROOT; };
+		CDEECC4C2C5069C0000C4392 /* cli.cxx */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = cli.cxx; sourceTree = "<group>"; };
+		CDEECC4D2C5069C0000C4392 /* cli.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cli.h; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -15,6 +43,8 @@
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CDEECC472C5050FD000C4392 /* libcurses.tbd in Frameworks */,
+				CD93A7E22C5016A500754263 /* libncurses.tbd in Frameworks */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -24,7 +54,11 @@
 		CD93A7C42C5015C000754263 = {
 			isa = PBXGroup;
 			children = (
+				CD93A7D52C5015EF00754263 /* ncurses-wrapper */,
+				CD93A7D62C5015F900754263 /* include */,
+				CD93A7D42C5015E300754263 /* src */,
 				CD93A7CE2C5015C000754263 /* Products */,
+				CD93A7E02C5016A500754263 /* Frameworks */,
 			);
 			sourceTree = "<group>";
 		};
@@ -36,6 +70,47 @@
 			name = Products;
 			sourceTree = "<group>";
 		};
+		CD93A7D42C5015E300754263 /* src */ = {
+			isa = PBXGroup;
+			children = (
+				CD93A7E52C501C0400754263 /* attribute_scope.cxx */,
+				CDEECC4C2C5069C0000C4392 /* cli.cxx */,
+				CD93A7E92C501F6F00754263 /* color.cxx */,
+				CD93A7D82C50160900754263 /* window.cxx */,
+			);
+			path = src;
+			sourceTree = "<group>";
+		};
+		CD93A7D62C5015F900754263 /* include */ = {
+			isa = PBXGroup;
+			children = (
+				CD93A7D72C5015F900754263 /* ncurses-wrapper */,
+			);
+			path = include;
+			sourceTree = "<group>";
+		};
+		CD93A7D72C5015F900754263 /* ncurses-wrapper */ = {
+			isa = PBXGroup;
+			children = (
+				CD93A7E62C501C0400754263 /* attribute_scope.h */,
+				CD93A7DE2C50165300754263 /* bounds.h */,
+				CDEECC4D2C5069C0000C4392 /* cli.h */,
+				CD93A7EA2C501F6F00754263 /* color.h */,
+				CD93A7E32C5016F400754263 /* forward.h */,
+				CD93A7DC2C50161700754263 /* window.h */,
+			);
+			path = "ncurses-wrapper";
+			sourceTree = "<group>";
+		};
+		CD93A7E02C5016A500754263 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+				CDEECC462C5050FD000C4392 /* libcurses.tbd */,
+				CD93A7E12C5016A500754263 /* libncurses.tbd */,
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXHeadersBuildPhase section */
@@ -43,6 +118,12 @@
 			isa = PBXHeadersBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CD93A7EC2C501F6F00754263 /* color.h in Headers */,
+				CD93A7E42C5016F400754263 /* forward.h in Headers */,
+				CD93A7DD2C50161700754263 /* window.h in Headers */,
+				CDEECC4F2C5069C0000C4392 /* cli.h in Headers */,
+				CD93A7E82C501C0400754263 /* attribute_scope.h in Headers */,
+				CD93A7DF2C50165300754263 /* bounds.h in Headers */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -103,6 +184,10 @@
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
 			files = (
+				CDEECC4E2C5069C0000C4392 /* cli.cxx in Sources */,
+				CD93A7EB2C501F6F00754263 /* color.cxx in Sources */,
+				CD93A7DA2C50160900754263 /* window.cxx in Sources */,
+				CD93A7E72C501C0400754263 /* attribute_scope.cxx in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -115,7 +200,7 @@
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_CXX_LANGUAGE_STANDARD = "c++20";
 				CLANG_ENABLE_MODULES = YES;
 				CLANG_ENABLE_OBJC_ARC = YES;
 				CLANG_ENABLE_OBJC_WEAK = YES;
@@ -159,6 +244,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
+				HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/usr/local/include";
 				MACOSX_DEPLOYMENT_TARGET = 12.3;
 				MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
 				MTL_FAST_MATH = YES;
@@ -173,7 +259,7 @@
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
+				CLANG_CXX_LANGUAGE_STANDARD = "c++20";
 				CLANG_ENABLE_MODULES = YES;
 				CLANG_ENABLE_OBJC_ARC = YES;
 				CLANG_ENABLE_OBJC_WEAK = YES;
@@ -211,6 +297,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
+				HEADER_SEARCH_PATHS = "$(BUILT_PRODUCTS_DIR)/usr/local/include";
 				MACOSX_DEPLOYMENT_TARGET = 12.3;
 				MTL_ENABLE_DEBUG_INFO = NO;
 				MTL_FAST_MATH = YES;

+ 25 - 0
src/attribute_scope.cxx

@@ -0,0 +1,25 @@
+//
+//  attribute_scope.cxx
+//  ncurses-wrapper
+//
+//  Created by Sam Jaffe on 7/23/24.
+//
+
+#include "ncurses-wrapper/attribute_scope.h"
+
+#include "ncurses-wrapper/window.h"
+
+namespace curses {
+AttributeScope::AttributeScope(std::vector<NCURSES_ATTR_T> init, Window &window)
+    : window_(&window), self_(init.rbegin(), init.rend()) {
+  for (attr_t attrib : init) {
+    good_ = good_ && (wattron(window_->self_, attrib) != ERR);
+  }
+}
+
+AttributeScope::~AttributeScope() {
+  for (attr_t attrib : self_) {
+    wattroff(window_->self_, attrib);
+  }
+}
+}

+ 74 - 0
src/cli.cxx

@@ -0,0 +1,74 @@
+//
+//  cli.cpp
+//  ncurses-wrapper
+//
+//  Created by Sam Jaffe on 7/23/24.
+//
+
+#include "ncurses-wrapper/cli.h"
+
+#include "ncurses-wrapper/window.h"
+
+namespace curses {
+void Cli::loop(std::function<void(curses::Window &, std::string)> on_enter) {
+  window_.printf("%s", prompt_.c_str());
+  while (true) {
+    int ch;
+    switch (ch = window_.getch()) {
+    case KEY_UP:
+      if (vertical_offset_ + 1 < stack_.size()) {
+        ++vertical_offset_;
+        horizontal_offset_ = 0;
+        window_.clear_line();
+        window_.printf("%s%s", prompt_.c_str(), get().c_str());
+      }
+      break;
+    case KEY_DOWN:
+      if (vertical_offset_ > 0) {
+        --vertical_offset_;
+        horizontal_offset_ = 0;
+        window_.clear_line();
+        window_.printf("%s%s", prompt_.c_str(), get().c_str());
+      }
+      break;
+    case KEY_LEFT:
+      if (horizontal_offset_ + 1 < get().size()) {
+        ++horizontal_offset_;
+        window_.move(Offset{-1, 0});
+      }
+      break;
+    case KEY_RIGHT:
+      if (horizontal_offset_ > 0) {
+        --horizontal_offset_;
+        window_.move(Offset{1, 0});
+      }
+      break;
+    case KEY_BACKSPACE:
+    case KEY_DC:
+    case 127:
+      if (horizontal_offset_ + 1 < get().size()) {
+        get().erase(get().end() - horizontal_offset_ - 1);
+      }
+      window_.move(Offset{-1, 0});
+      window_.delch();
+      break;
+    case 10:
+    case KEY_ENTER:
+      window_.printf("\n");
+      on_enter(window_, get());
+      stack_.emplace_back();
+      window_.printf("%s", prompt_.c_str());
+      break;
+    default:
+      get().insert(get().size() - horizontal_offset_, 1, ch);
+      window_.printf("%c", ch);
+      if (horizontal_offset_ > 0) {
+        window_.printf("%s", get().c_str() + get().size() - horizontal_offset_);
+        window_.move(Offset{-horizontal_offset_, 0});
+      }
+      break;
+    }
+  }
+}
+
+}

+ 51 - 0
src/color.cxx

@@ -0,0 +1,51 @@
+//
+//  color.cxx
+//  ncurses-wrapper
+//
+//  Created by Sam Jaffe on 7/23/24.
+//
+
+#include "ncurses-wrapper/color.h"
+
+#include <curses.h>
+
+#include <map>
+
+namespace curses {
+Color const Color::DEFAULT = {-1, -1, -1};
+Color const Color::BLACK = {0, 0, 0};
+Color const Color::RED = {1000, 0, 0};
+Color const Color::GREEN = {0, 1000, 0};
+Color const Color::YELLOW = {1000, 1000, 0};
+Color const Color::BLUE = {0, 0, 1000};
+Color const Color::MAGENTA = {1000, 0, 1000};
+Color const Color::CYAN = {0, 1000, 1000};
+Color const Color::WHITE = {1000, 1000, 1000};
+
+std::map<Color, short> g_codes{
+  {Color::DEFAULT, -1}, // Only Default this one... for some reason RED behaves weirdly...
+};
+std::map<ColorPair, int> g_pairs;
+
+short Color::to_code() const {
+  if (auto it = g_codes.find(*this); it != g_codes.end()) {
+    return it->second;
+  }
+  short code = g_codes.size();
+  init_color(code, red, green, blue);
+  g_codes.emplace(*this, code);
+  return code;
+}
+
+int ColorPair::to_code() const {
+  if (auto it = g_pairs.find(*this); it != g_pairs.end()) {
+    return it->second;
+  }
+  
+  int code = static_cast<int>(g_pairs.size() + 1);
+  g_pairs.emplace(*this, code);
+  init_pair(code, foreground.to_code(), background.to_code());
+  return code;
+}
+
+}

+ 59 - 0
src/window.cxx

@@ -0,0 +1,59 @@
+//
+//  window.cxx
+//  ncurses-wrapper
+//
+//  Created by Sam Jaffe on 7/23/24.
+//
+
+#include "ncurses-wrapper/window.h"
+
+#include <ncurses.h>
+
+#include "ncurses-wrapper/color.h"
+
+#undef getch
+#undef delch
+
+namespace curses {
+void Window::init_with(NoEcho_t) { noecho(); }
+void Window::init_with(WithColor_t) { start_color(); use_default_colors(); }
+void Window::init_with(ExtendedKeys_t) { keypad(self_, TRUE); }
+}
+
+namespace curses {
+NCURSES_ATTR_T Window::to_attr(Bold_t) const { return A_BOLD; }
+NCURSES_ATTR_T Window::to_attr(ColorPair const &color) const {
+  return COLOR_PAIR(color.to_code());
+}
+}
+
+namespace curses {
+Window::~Window() {
+  if (self_ == stdscr) {
+    endwin();
+  }
+}
+
+Status Window::printf(char const *fmt, ...) {
+  va_list args;
+  va_start(args, fmt);
+  int result = vwprintw(self_, fmt, args);
+  va_end(args);
+  return to_status(result);
+}
+
+void Window::move(Position const &pos) { wmove(self_, pos.y, pos.x); }
+void Window::move(Offset const &off) { move(getpos() + off); }
+Position Window::getpos() { return {.x = getcurx(self_), .y = getcury(self_)}; }
+Bounds Window::getsize() {
+  return {.width = getmaxx(self_), .height = getmaxy(self_)};
+}
+
+int Window::getch() { return wgetch(self_); }
+int Window::delch() { return wdelch(self_); }
+void Window::clear_line() {
+  move(getpos().only_y());
+  wclrtoeol(self_);
+}
+void Window::clear() { wclear(self_); }
+}