1 //: Take raw control of the text-mode display and console, putting it in
  2 //: 'console' mode rather than the usual automatically-scrolling 'typewriter'
  3 //: mode.
  4 
  5 //:: Display management
  6 
  7 :(before "End Globals")
  8 int Display_row = 0;
  9 int Display_column = 0;
 10 
 11 :(before "End Includes")
 12 #define CHECK_SCREEN \
 13     if (!tb_is_active()) { \
 14       if (Run_tests) \
 15         raise << maybe(current_recipe_name()) << "tried to print to real screen in a test!\n" << end(); \
 16       else \
 17         raise << maybe(current_recipe_name()) << "tried to print to real screen before 'open-console' or after 'close-console'\n" << end(); \
 18       break; \
 19     }
 20 #define CHECK_CONSOLE \
 21     if (!tb_is_active()) { \
 22       if (Run_tests) \
 23         raise << maybe(current_recipe_name()) << "tried to read event from real keyboard/mouse in a test!\n" << end(); \
 24       else \
 25         raise << maybe(current_recipe_name()) << "tried to read event from real keyboard/mouse before 'open-console' or after 'close-console'\n" << end(); \
 26       break; \
 27     }
 28 
 29 :(before "End Primitive Recipe Declarations")
 30 OPEN_CONSOLE,
 31 :(before "End Primitive Recipe Numbers")
 32 put(Recipe_ordinal, "open-console", OPEN_CONSOLE);
 33 :(before "End Primitive Recipe Checks")
 34 case OPEN_CONSOLE: {
 35   break;
 36 }
 37 :(before "End Primitive Recipe Implementations")
 38 case OPEN_CONSOLE: {
 39   tb_init();
 40   std::setvbuf(stdout, NULL, _IONBF, 0);  // disable buffering in cout
 41   Display_row = Display_column = 0;
 42   int width = tb_width();
 43   int height = tb_height();
 44   if (width > 222 || height > 222) {
 45     if (width > 222)
 46       raise << "sorry, Mu doesn't support windows wider than 222 characters in console mode. Please resize your window.\n" << end();
 47     if (height > 222)
 48       raise << "sorry, Mu doesn't support windows taller than 222 characters in console mode. Please resize your window.\n" << end();
 49     exit(1);
 50   }
 51   break;
 52 }
 53 
 54 :(before "End Primitive Recipe Declarations")
 55 CLOSE_CONSOLE,
 56 :(before "End Primitive Recipe Numbers")
 57 put(Recipe_ordinal, "close-console", CLOSE_CONSOLE);
 58 :(before "End Primitive Recipe Checks")
 59 case CLOSE_CONSOLE: {
 60   break;
 61 }
 62 :(before "End Primitive Recipe Implementations")
 63 case CLOSE_CONSOLE: {
 64   tb_shutdown();
 65   break;
 66 }
 67 
 68 :(before "End Primitive Recipe Declarations")
 69 CLEAR_DISPLAY,
 70 :(before "End Primitive Recipe Numbers")
 71 put(Recipe_ordinal, "clear-display", CLEAR_DISPLAY);
 72 :(before "End Primitive Recipe Checks")
 73 case CLEAR_DISPLAY: {
 74   break;
 75 }
 76 :(before "End Primitive Recipe Implementations")
 77 case CLEAR_DISPLAY: {
 78   CHECK_SCREEN;
 79   tb_clear();
 80   Display_row = Display_column = 0;
 81   break;
 82 }
 83 
 84 :(before "End Primitive Recipe Declarations")
 85 PRINT_CHARACTER_TO_DISPLAY,
 86 :(before "End Primitive Recipe Numbers")
 87 put(Recipe_ordinal, "print-character-to-display", PRINT_CHARACTER_TO_DISPLAY);
 88 :(before "End Primitive Recipe Checks")
 89 case PRINT_CHARACTER_TO_DISPLAY: {
 90   if (inst.ingredients.empty()) {
 91     raise << maybe(get(Recipe, r).name) << "'print-character-to-display' requires at least one ingredient, but got '" << to_original_string(inst) << "'\n" << end();
 92     break;
 93   }
 94   if (!is_mu_number(inst.ingredients.at(0))) {
 95     raise << maybe(get(Recipe, r).name) << "first ingredient of 'print-character-to-display' should be a character, but got '" << inst.ingredients.at(0).original_string << "'\n" << end();
 96     break;
 97   }
 98   if (SIZE(inst.ingredients) > 1) {
 99     if (!is_mu_number(inst.ingredients.at(1))) {
100       raise << maybe(get(Recipe, r).name) << "second ingredient of 'print-character-to-display' should be a foreground color number, but got '" << inst.ingredients.at(1).original_string << "'\n" << end();
101       break;
102     }
103   }
104   if (SIZE(inst.ingredients) > 2) {
105     if (!is_mu_number(inst.ingredients.Now().UTC().Format("2006-01-02")

	// Flags to parse for apod service.
	apodAPI = apodCmd.String("api", "https://api.nasa.gov/planetary/apod", "APOD API link")
	apodKey = apodCmd.String("api-key", "DEMO_KEY", "NASA API Key for expanded usage")
	apodDate = apodCmd.String("date", defDate, "Date of NASA APOD to retrieve")
	apodRand = apodCmd.Bool("random", false, "Choose a date random starting from 1995-06-16")
	apodPathOnly = apodCmd.Bool("path-only", false, "Print only the path")
	apodQuiet = apodCmd.Bool("quiet", false, "Stay quiet")
	apodDump = apodCmd.Bool("dump", false, "Dump received response")
	apodNotify = apodCmd.Bool("notify", false, "Send a desktop notification with background information")

	// Flags to parse for bpod service.
	bpodAPI = bpodCmd.String("api", "https://www.bing.com/HPImageArchive.aspx", "BPOD API")
	bpodRand = bpodCmd.Bool("random", false, "Choose a random image from last week's BPOD")
	bpodPathOnly = bpodCmd.Bool("path-only", false, "Print only the path")
	bpodQuiet = bpodCmd.Bool("quiet", false, "Stay quiet")
	bpodDump = bpodCmd.Bool("dump", false, "Dump received response")
	bpodNotify = bpodCmd.Bool("notify", false, "Send a desktop notification with background information")

	// Switching on commands will cause more repetition than
	// switching on service. If we switch on commands then switch
	// on service will have to be replicated on every command
	// switch. Reverse is also true, this way we will repeat
	// command switch in every service but we do so in a better
	// way.
	//
	// However we check if the correct command was passed. version
	// command is not included because it has been dealt with
	// earlier in the program & the program should've exited after
	// that, if it reaches here then it's an error.
	switch os.Args[1] {
	case "set", "fetch":
	default:
		fmt.Printf("Invalid command: %q\n", os.Args[1])
		printUsage()
		os.Exit(1)
	}

	switch os.Args[2] {
	case "apod", "nasa":
		apodCmd.Parse(os.Args[3:])
		if apodCmd.Parsed() {
			execAPOD()
		}
	case "bpod", "bing":
		bpodCmd.Parse(os.Args[3:])
		if bpodCmd.Parsed() {
			execBPOD()
		}
	default:
		fmt.Printf("Invalid service: %q\n", os.Args[2])
		printUsage()
		os.Exit(1)
	}
}

func printUsage() {
	fmt.Println("Usage: cetus <command> <service> [<flags>]\n")
	fmt.Println("Commands: ")
	fmt.Println(" set     Set the latest image as background")
	fmt.Println(" fetch   Fetch the latest image information")
	fmt.Println(" version Print version")
	fmt.Println("\nServices: ")
	fmt.Println(" apod   NASA Astronomy Picture of the Day")
	fmt.Println(" bpod   Bing Photo of the Day")
}

// Check whether user has set CETUS_CACHE_DIR, if not then use the
// XDG_CACHE_HOME. If XDG_CACHE_HOME is not set then $HOME/.config
// should be used, according to XDG Base Directory Specification
func getCacheDir() string {
	cacheDir := os.Getenv("CETUS_CACHE_DIR")
	if len(cacheDir) == 0 {
		cacheDir = os.Getenv("XDG_CACHE_HOME")
	}
	if len(cacheDir) == 0 {
		cacheDir = fmt.Sprintf("%s/%s/%s", os.Getenv("HOME"),
			".cache", "cetus")
	}
	return cacheDir
}

func chkErr(err error) {
	if err != nil {
		fmt.Println(err)
		os.Exit(1)
	}
}
n class="Delimiter">:(before "End Primitive Recipe Implementations") 204 case MOVE_CURSOR_DOWN_ON_DISPLAY: { 205 CHECK_SCREEN; 206 int h=tb_height(); 207 int height = (h >= 0) ? h : 0; 208 if (Display_row < height-1) { 209 ++Display_row; 210 tb_set_cursor(Display_column, Display_row); 211 } 212 break; 213 } 214 215 :(before "End Primitive Recipe Declarations") 216 MOVE_CURSOR_UP_ON_DISPLAY, 217 :(before "End Primitive Recipe Numbers") 218 put(Recipe_ordinal, "move-cursor-up-on-display", MOVE_CURSOR_UP_ON_DISPLAY); 219 :(before "End Primitive Recipe Checks") 220 case MOVE_CURSOR_UP_ON_DISPLAY: { 221 break; 222 } 223 :(before "End Primitive Recipe Implementations") 224 case MOVE_CURSOR_UP_ON_DISPLAY: { 225 CHECK_SCREEN; 226 if (Display_row > 0) { 227 --Display_row; 228 tb_set_cursor(Display_column, Display_row); 229 } 230 break; 231 } 232 233 :(before "End Primitive Recipe Declarations") 234 MOVE_CURSOR_RIGHT_ON_DISPLAY, 235 :(before "End Primitive Recipe Numbers") 236 put(Recipe_ordinal, "move-cursor-right-on-display", MOVE_CURSOR_RIGHT_ON_DISPLAY); 237 :(before "End Primitive Recipe Checks") 238 case MOVE_CURSOR_RIGHT_ON_DISPLAY: { 239 break; 240 } 241 :(before "End Primitive Recipe Implementations") 242 case MOVE_CURSOR_RIGHT_ON_DISPLAY: { 243 CHECK_SCREEN; 244 int w=tb_width(); 245 int width = (w >= 0) ? w : 0; 246 if (Display_column < width-1) { 247 ++Display_column; 248 tb_set_cursor(Display_column, Display_row); 249 } 250 break; 251 } 252 253 :(before "End Primitive Recipe Declarations") 254 MOVE_CURSOR_LEFT_ON_DISPLAY, 255 :(before "End Primitive Recipe Numbers") 256 put(Recipe_ordinal, "move-cursor-left-on-display", MOVE_CURSOR_LEFT_ON_DISPLAY); 257 :(before "End Primitive Recipe Checks") 258 case MOVE_CURSOR_LEFT_ON_DISPLAY: { 259 break; 260 } 261 :(before "End Primitive Recipe Implementations") 262 case MOVE_CURSOR_LEFT_ON_DISPLAY: { 263 CHECK_SCREEN; 264 if (Display_column > 0) { 265 --Display_column; 266 tb_set_cursor(Display_column, Display_row); 267 } 268 break; 269 } 270 271 //: as a convenience, make $print mostly work in console mode 272 :(before "End $print 10/newline Special-cases") 273 else if (tb_is_active()) { 274 move_cursor_to_start_of_next_line_on_display(); 275 } 276 :(code) 277 void move_cursor_to_start_of_next_line_on_display() { 278 if (Display_row < tb_height()-1) ++Display_row; 279 else Display_row = 0; 280 Display_column = 0; 281 tb_set_cursor(Display_column, Display_row); 282 } 283 284 :(before "End Primitive Recipe Declarations") 285 DISPLAY_WIDTH, 286 :(before "End Primitive Recipe Numbers") 287 put(Recipe_ordinal, "display-width", DISPLAY_WIDTH); 288 :(before "End Primitive Recipe Checks") 289 case DISPLAY_WIDTH: { 290 break; 291 } 292 :(before "End Primitive Recipe Implementations") 293 case DISPLAY_WIDTH: { 294 CHECK_SCREEN; 295 products.resize(1); 296 products.at(0).push_back(tb_width()); 297 break; 298 } 299 300 :(before "End Primitive Recipe Declarations") 301 DISPLAY_HEIGHT, 302 :(before "End Primitive Recipe Numbers") 303 put(Recipe_ordinal, "display-height", DISPLAY_HEIGHT); 304 :(before "End Primitive Recipe Checks") 305 case DISPLAY_HEIGHT: { 306 break; 307 } 308 :(before "End Primitive Recipe Implementations") 309 case DISPLAY_HEIGHT: { 310 CHECK_SCREEN; 311 products.resize(1); 312 products.at(0).push_back(tb_height()); 313 break; 314 } 315 316 //:: Keyboard/mouse management 317 318 :(before "End Primitive Recipe Declarations") 319 WAIT_FOR_SOME_INTERACTION, 320 :(before "End Primitive Recipe Numbers") 321 put(Recipe_ordinal, "wait-for-some-interaction", WAIT_FOR_SOME_INTERACTION); 322 :(before "End Primitive Recipe Checks") 323 case WAIT_FOR_SOME_INTERACTION: { 324 break; 325 } 326 :(before "End Primitive Recipe Implementations") 327 case WAIT_FOR_SOME_INTERACTION: { 328 CHECK_SCREEN; 329 tb_event event; 330 tb_poll_event(&event); 331 break; 332 } 333 334 :(before "End Primitive Recipe Declarations") 335 CHECK_FOR_INTERACTION, 336 :(before "End Primitive Recipe Numbers") 337 put(Recipe_ordinal, "check-for-interaction", CHECK_FOR_INTERACTION); 338 :(before "End Primitive Recipe Checks") 339 case CHECK_FOR_INTERACTION: { 340 break; 341 } 342 :(before "End Primitive Recipe Implementations") 343 case CHECK_FOR_INTERACTION: { 344 CHECK_CONSOLE; 345 products.resize(2); // result and status 346 tb_event event; 347 int event_type = tb_peek_event(&event, 5/*ms*/); 348 if (event_type == TB_EVENT_KEY && event.ch) { 349 products.at(0).push_back(/*text event*/0); 350 products.at(0).push_back(event.ch); 351 products.at(0).push_back(0); 352 products.at(0).push_back(0); 353 products.at(1).push_back(/*found*/true); 354 break; 355 } 356 // treat keys within ascii as unicode characters 357 if (event_type == TB_EVENT_KEY && event.key < 0xff) { 358 products.at(0).push_back(/*text event*/0); 359 if (event.key == TB_KEY_CTRL_C) exit(1); 360 if (event.key == TB_KEY_BACKSPACE2) event.key = TB_KEY_BACKSPACE; 361 if (event.key == TB_KEY_CARRIAGE_RETURN) event.key = TB_KEY_NEWLINE; 362 products.at(0).push_back(event.key); 363 products.at(0).push_back(0); 364 products.at(0).push_back(0); 365 products.at(1).push_back(/*found*/true); 366 break; 367 } 368 // keys outside ascii aren't unicode characters but arbitrary termbox inventions 369 if (event_type == TB_EVENT_KEY) { 370 products.at(0).push_back(/*keycode event*/1); 371 products.at(0).push_back(event.key); 372 products.at(0).push_back(0); 373 products.at(0).push_back(0); 374 products.at(1).push_back(/*found*/true); 375 break; 376 } 377 if (event_type == TB_EVENT_MOUSE) { 378 products.at(0).push_back(/*touch event*/2); 379 products.at(0).push_back(event.key); // which button, etc. 380 products.at(0).push_back(event.y); // row 381 products.at(0).push_back(event.x); // column 382 products.at(1).push_back(/*found*/true); 383 break; 384 } 385 if (event_type == TB_EVENT_RESIZE) { 386 products.at(0).push_back(/*resize event*/3); 387 products.at(0).push_back(event.w); // width 388 products.at(0).push_back(event.h); // height 389 products.at(0).push_back(0); 390 products.at(1).push_back(/*found*/true); 391 break; 392 } 393 assert(event_type == 0); 394 products.at(0).push_back(0); 395 products.at(0).push_back(0); 396 products.at(0).push_back(0); 397 products.at(0).push_back(0); 398 products.at(1).push_back(/*found*/false); 399 break; 400 } 401 402 :(before "End Primitive Recipe Declarations") 403 INTERACTIONS_LEFT, 404 :(before "End Primitive Recipe Numbers") 405 put(Recipe_ordinal, "interactions-left?", INTERACTIONS_LEFT); 406 :(before "End Primitive Recipe Checks") 407 case INTERACTIONS_LEFT: { 408 break; 409 } 410 :(before "End Primitive Recipe Implementations") 411 case INTERACTIONS_LEFT: { 412 CHECK_CONSOLE; 413 products.resize(1); 414 products.at(0).push_back(tb_event_ready()); 415 break; 416 } 417 418 //: hacks to make text-mode apps more responsive under Unix 419 420 :(before "End Primitive Recipe Declarations") 421 CLEAR_LINE_ON_DISPLAY, 422 :(before "End Primitive Recipe Numbers") 423 put(Recipe_ordinal, "clear-line-on-display", CLEAR_LINE_ON_DISPLAY); 424 :(before "End Primitive Recipe Checks") 425 case CLEAR_LINE_ON_DISPLAY: { 426 break; 427 } 428 :(before "End Primitive Recipe Implementations") 429 case CLEAR_LINE_ON_DISPLAY: { 430 CHECK_SCREEN; 431 int width = tb_width(); 432 for (int x = Display_column; x < width; ++x) 433 tb_print(' ', TB_WHITE, TB_BLACK); 434 tb_set_cursor(Display_column, Display_row); 435 break; 436 } 437 438 :(before "End Primitive Recipe Declarations") 439 CLEAR_DISPLAY_FROM, 440 :(before "End Primitive Recipe Numbers") 441 put(Recipe_ordinal, "clear-display-from", CLEAR_DISPLAY_FROM); 442 :(before "End Primitive Recipe Checks") 443 case CLEAR_DISPLAY_FROM: { 444 break; 445 } 446 :(before "End Primitive Recipe Implementations") 447 case CLEAR_DISPLAY_FROM: { 448 CHECK_SCREEN; 449 // todo: error checking 450 int row = ingredients.at(0).at(0); 451 int column = ingredients.at(1).at(0); 452 int left = ingredients.at(2).at(0); 453 int right = ingredients.at(3).at(0); 454 int height=tb_height(); 455 for (/*nada*/; row < height; ++row, column=left) { // start column from left in every inner loop except first 456 tb_set_cursor(column, row); 457 for (/*nada*/; column <= right; ++column) 458 tb_print(' ', TB_WHITE, TB_BLACK); 459 } 460 tb_set_cursor(Display_column, Display_row); 461 break; 462 }