Compare commits
10 commits
d14ae17ad8
...
974668c1a0
Author | SHA1 | Date | |
---|---|---|---|
974668c1a0 | |||
|
e64cc0461d | ||
|
c3c2c78736 | ||
|
9d3e603b4a | ||
|
22af12fffe | ||
|
c195083fe8 | ||
|
4cf11f475d | ||
|
7bfee64b45 | ||
|
db283591c0 | ||
|
b2f0b55be4 |
4 changed files with 177 additions and 31 deletions
3
Makefile
Normal file
3
Makefile
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
wf-market:
|
||||||
|
python -m nuitka tui.py
|
||||||
|
mv tui.bin wf-market && rm tui.build -r
|
9
README.md
Normal file
9
README.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
# wf-market
|
||||||
|
|
||||||
|
Terminal based interface (ncurses) for quick-copying orders on warframe.market.
|
||||||
|
|
||||||
|
Press `s` to go back to the search bar. Press `i` to go back to the item listing on the left. Sell orders on the left, Buy orders on the right. `Return` to copy a whisper to clipboard.
|
||||||
|
|
||||||
|
Requires python and python-requests.
|
||||||
|
|
||||||
|
Copying requires xclip.
|
12
api.py
12
api.py
|
@ -1,12 +0,0 @@
|
||||||
import requests
|
|
||||||
import os, getpass, re, pickle, getpass, json
|
|
||||||
|
|
||||||
class Session():
|
|
||||||
def __init__(self):
|
|
||||||
self.client = requests.Session()
|
|
||||||
|
|
||||||
def api_request(self, url='/', method='get', **kwargs):
|
|
||||||
return json.loads(getattr(self.client, method)('https://api.warframe.market/v1' + url, **kwargs).text)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
184
tui.py
184
tui.py
|
@ -1,7 +1,21 @@
|
||||||
import curses
|
import requests
|
||||||
import curses.textpad
|
|
||||||
import curses.ascii
|
|
||||||
|
|
||||||
|
import curses, curses.textpad
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
|
||||||
|
# API access handler
|
||||||
|
class Session():
|
||||||
|
def __init__(self):
|
||||||
|
self.client = requests.Session()
|
||||||
|
|
||||||
|
def api_request(self, url='/', method='get', **kwargs):
|
||||||
|
# read only, errors should force crash with no damage
|
||||||
|
return json.loads(getattr(self.client, method)('https://api.warframe.market/v1' + url, **kwargs).text)
|
||||||
|
|
||||||
|
|
||||||
|
# main terminal element renderer
|
||||||
class GUI():
|
class GUI():
|
||||||
def __init__(self, root, h, w, y, x):
|
def __init__(self, root, h, w, y, x):
|
||||||
self.root = root
|
self.root = root
|
||||||
|
@ -9,38 +23,170 @@ class GUI():
|
||||||
|
|
||||||
def create_gui(self):
|
def create_gui(self):
|
||||||
self.create_search_bar()
|
self.create_search_bar()
|
||||||
self.create_search_results()
|
self.create_item_search_results()
|
||||||
|
self.create_order_search_results()
|
||||||
|
|
||||||
|
self.root.refresh()
|
||||||
|
|
||||||
def create_search_bar(self):
|
def create_search_bar(self):
|
||||||
self.search_box = self.window.subwin(3, self.root.getmaxyx()[1], 0, 0)
|
self.search_box = self.window.subwin(3, self.window.getmaxyx()[1], 0, 0) # 2 extra lines for border
|
||||||
self.search_box.border()
|
self.search_box.border()
|
||||||
self.search_bar = self.window.subwin(1, self.root.getmaxyx()[1] - 2, 1, 1)
|
self.search_bar = self.window.subwin(1, self.window.getmaxyx()[1] - 2, 1, 1) # actual search box
|
||||||
self.search_in = curses.textpad.Textbox(self.search_bar)
|
self.search_in = curses.textpad.Textbox(self.search_bar) # just cursed nested vars at this point, really only need this one for searching
|
||||||
|
|
||||||
def create_search_results(self):
|
def create_item_search_results(self):
|
||||||
self.results_box = self.window.subwin(self.root.getmaxyx()[0] - 3, self.root.getmaxyx()[1] // 3, 3, 0)
|
self.results_box = self.window.subwin(self.window.getmaxyx()[0] - 3, self.window.getmaxyx()[1] // 3, 3, 0) # skip y of search box, occupy left 1/3 of screen
|
||||||
self.results_box.border()
|
self.results_box.border()
|
||||||
|
|
||||||
|
def create_order_search_results(self):
|
||||||
|
self.orders_box = self.window.subwin(self.window.getmaxyx()[0] - 3, self.window.getmaxyx()[1] * 2 // 3, 3, self.window.getmaxyx()[1] // 3) # see item search
|
||||||
|
self.orders_box.border()
|
||||||
|
|
||||||
|
# central class handling states and user interactions
|
||||||
class App():
|
class App():
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.gui = GUI(self.root, *self.root.getmaxyx(), 0, 0)
|
self.gui = GUI(self.root, *self.root.getmaxyx(), 0, 0)
|
||||||
|
self.client = Session()
|
||||||
|
self.item_list = self.client.api_request(url = '/items').get('payload').get('items') # fetch all items as cache on start
|
||||||
|
|
||||||
def show_gui(self):
|
def show_gui(self):
|
||||||
self.root.clear()
|
self.root.clear()
|
||||||
self.gui.create_gui()
|
self.gui.create_gui()
|
||||||
|
|
||||||
|
def update_item_search_results(self):
|
||||||
|
search_str = self.gui.search_in.gather() # read current search box text, match item list to show, truncate at gui height, no scrolling implemented
|
||||||
|
items = [item for item in self.item_list if search_str.strip().lower() in item.get('item_name').lower()][:self.gui.results_box.getmaxyx()[0] - 2]
|
||||||
|
self.gui.results_box.clear()
|
||||||
|
self.gui.results_box.border()
|
||||||
|
for i in range(len(items)):
|
||||||
|
try:
|
||||||
|
self.gui.results_box.addstr(i + 1, 1, items[i]['item_name'][:self.gui.results_box.getmaxyx()[1] - 2]) # display name, pos+border adjust, truncate at gui width
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
self.gui.results_box.refresh()
|
||||||
|
self.items = items # for selection
|
||||||
|
|
||||||
|
def update_orders_results(self):
|
||||||
|
# query API for orders with item, sort by plat
|
||||||
|
orders = sorted(self.client.api_request(url = f'/items/{self.selected_item["url_name"]}/orders').get('payload').get('orders'), key=lambda x: x['platinum'])
|
||||||
|
# restrict to buy and online in-game, truncate at height
|
||||||
|
self.buy_orders = [order for order in orders if order['order_type'] == 'buy' and order['user']['status'] == 'ingame'][:self.gui.orders_box.getmaxyx()[0] - 2]
|
||||||
|
self.buy_orders.reverse() # reverse sort for sells
|
||||||
|
# restrict to sells and online in-game, truncate at height
|
||||||
|
self.sell_orders = [order for order in orders if order['order_type'] == 'sell' and order['user']['status'] == 'ingame'][:self.gui.orders_box.getmaxyx()[0] - 2]
|
||||||
|
|
||||||
|
self.gui.orders_box.clear()
|
||||||
|
self.gui.orders_box.border()
|
||||||
|
# display item listing str for each order
|
||||||
|
for i in range(len(self.sell_orders)):
|
||||||
|
try:
|
||||||
|
self.gui.orders_box.addstr(i + 1, 1, '{:20.20} {: >3}p x{: <3} {: <10}'.format(self.sell_orders[i]['user']['ingame_name'], self.sell_orders[i]['platinum'], self.sell_orders[i]['quantity'], self.sell_orders[i].get('mod_rank', '')))
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
for i in range(len(self.buy_orders)):
|
||||||
|
try:
|
||||||
|
self.gui.orders_box.addstr(i + 1, self.gui.orders_box.getmaxyx()[1] // 2, '{:20.20} {: >3}p x{: <3} {: <10}'.format(self.buy_orders[i]['user']['ingame_name'], self.buy_orders[i]['platinum'], self.buy_orders[i]['quantity'], self.buy_orders[i].get('mod_rank', '')))
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
self.gui.orders_box.refresh()
|
||||||
|
|
||||||
|
def decide_state(self):
|
||||||
|
# decide next action and input state
|
||||||
|
if self.state == 0:
|
||||||
|
return self.search
|
||||||
|
elif self.state == 1:
|
||||||
|
return self.select_item
|
||||||
|
elif self.state == 2:
|
||||||
|
return self.select_order
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
# initialize at search state
|
||||||
|
self.show_gui()
|
||||||
|
self.state = 0
|
||||||
|
while True:
|
||||||
|
self.decide_state()()
|
||||||
|
|
||||||
|
|
||||||
|
def search(self):
|
||||||
|
# when search box selected, allow edit, update item listing when user done
|
||||||
|
self.gui.search_in.edit()
|
||||||
|
self.update_item_search_results()
|
||||||
|
|
||||||
|
# move to item selection state
|
||||||
|
self.state = 1
|
||||||
|
|
||||||
|
def select_item(self):
|
||||||
|
# move cursor pos to first listing in search results, wait for intpu
|
||||||
|
self.gui.results_box.move(1, 1)
|
||||||
|
self.gui.results_box.refresh()
|
||||||
|
x = self.root.getch()
|
||||||
|
# implement item scrolling if enter or s
|
||||||
|
while x != 10 and x != 115:
|
||||||
|
if x == curses.KEY_UP:
|
||||||
|
self.gui.results_box.move((self.gui.results_box.getyx()[0] - 1 - 1) % len(self.items) + 1, 1)
|
||||||
|
elif x == curses.KEY_DOWN:
|
||||||
|
self.gui.results_box.move((self.gui.results_box.getyx()[0] - 1 + 1) % len(self.items) + 1, 1)
|
||||||
|
self.gui.results_box.refresh()
|
||||||
|
x = self.root.getch()
|
||||||
|
|
||||||
|
# on return
|
||||||
|
if x == 10:
|
||||||
|
# copy to clipboard, requires xclip obv, replace however, change for other, no idea why I wrote this part? only copy should be in order sel
|
||||||
|
subprocess.run(f'echo \'{self.gui.results_box.getyx()}\' | xclip -sel c', shell = True)
|
||||||
|
self.selected_item = self.items[self.gui.results_box.getyx()[0] - 1] # get cursor position, select corresponding item
|
||||||
|
self.update_orders_results() # update orders listing from selection
|
||||||
|
self.state = 2 # move to order selection
|
||||||
|
else:
|
||||||
|
self.state = 0 # otherwise s returns to search state
|
||||||
|
|
||||||
|
def select_order(self):
|
||||||
|
# move cursor to order listing
|
||||||
|
self.gui.orders_box.move(1, 1)
|
||||||
|
self.gui.orders_box.refresh()
|
||||||
|
x = self.root.getch()
|
||||||
|
col = 0 # for switching between buy and sell cols
|
||||||
|
|
||||||
|
# if not s, i or enter, implement selection
|
||||||
|
# same as items, but also left and right moves between buy and sell
|
||||||
|
while x != 10 and x != 115 and x != 105:
|
||||||
|
if x == curses.KEY_UP:
|
||||||
|
self.gui.orders_box.move((self.gui.orders_box.getyx()[0] - 1 - 1) % len(self.sell_orders if col == 0 else self.buy_orders) + 1, 1 if col == 0 else self.gui.orders_box.getmaxyx()[1] // 2)
|
||||||
|
elif x == curses.KEY_DOWN:
|
||||||
|
self.gui.orders_box.move((self.gui.orders_box.getyx()[0] - 1 + 1) % len(self.sell_orders if col == 0 else self.buy_orders) + 1, 1 if col == 0 else self.gui.orders_box.getmaxyx()[1] // 2)
|
||||||
|
elif x == curses.KEY_LEFT or x == curses.KEY_RIGHT:
|
||||||
|
col = int(not col)
|
||||||
|
self.gui.orders_box.move(min(self.gui.orders_box.getyx()[0], len(self.buy_orders if col else self.sell_orders)), 1 if col == 0 else self.gui.orders_box.getmaxyx()[1] // 2)
|
||||||
|
self.gui.orders_box.refresh()
|
||||||
|
x = self.root.getch()
|
||||||
|
|
||||||
|
# if enter, get index of order and copy whisper text
|
||||||
|
if x == 10:
|
||||||
|
self.selected_order = (self.buy_orders if col else self.sell_orders)[self.gui.orders_box.getyx()[0] - 1]
|
||||||
|
self.copy_whisper()
|
||||||
|
# if s, go back to search
|
||||||
|
elif x == 115:
|
||||||
|
self.state = 0
|
||||||
|
# elif i, go to item select
|
||||||
|
else:
|
||||||
|
self.state = 1
|
||||||
|
|
||||||
|
def copy_whisper(self):
|
||||||
|
# copy whisper to clipboard, again xclip, replace as needed, structure:
|
||||||
|
# WTB/WTS item_name listed for plat_val on warframe.market.
|
||||||
|
whisper = f'\\w {self.selected_order["user"]["ingame_name"]} {"WTS" if self.selected_order["order_type"] == "buy" else "WTB"} {self.selected_item["item_name"]} listed for {self.selected_order["platinum"]}p on warframe.market'
|
||||||
|
subprocess.run(f'echo \'{whisper}\' | xclip -sel c', shell = True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def main(w):
|
def main(w):
|
||||||
app = App(w)
|
app = App(w)
|
||||||
app.show_gui()
|
app.run()
|
||||||
w.refresh()
|
|
||||||
app.gui.search_in.edit()
|
|
||||||
search_string = app.gui.search_in.gather()
|
|
||||||
app.gui.results_box.addstr(1, 1, search_string)
|
|
||||||
app.gui.results_box.refresh()
|
|
||||||
w.getch()
|
|
||||||
return search_string
|
|
||||||
|
|
||||||
print(curses.wrapper(main))
|
try:
|
||||||
|
curses.wrapper(main) # runs main, curses handles exit wierdness
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print('Exiting')
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
print("No Internet")
|
||||||
|
sys.exit(1)
|
||||||
|
|
Loading…
Reference in a new issue